[RAM] Introduce maxScheduledPerMinute rule circuit breaker and route (#164791)

## Summary
Resolves: https://github.com/elastic/kibana/issues/162262

This PR is the backend changes to add a circuit breaker
`xpack.alerting.rules.maxScheduledPerMinute` to both serverless and
other environments that limits the number of rules to 400 runs / minute
and 10000 runs / minute, respectively. There will be another PR to
follow this one that gives the user UI hints when creating/editing rules
that go over this limit.

This circuit breaker check is applied to the following routes:
- Create Rule
- Update Rule
- Enable Rule
- Bulk Enable Rule
- Bulk Edit Rule

Also adds a new route: `/internal/alerting/rules/_schedule_frequency` to
get the current total schedules per minute (of enabled rules) and the
remaining interval allotment.

### 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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: lcawl <lcawley@elastic.co>
Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Jiawei Wu 2023-09-06 09:13:36 -07:00 committed by GitHub
parent 1a9bb19e57
commit 456f47f3ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 1773 additions and 138 deletions

View file

@ -171,6 +171,7 @@ enabled:
- x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group3/config_with_schedule_circuit_breaker.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts
- x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/config.ts
- x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/config.ts

View file

@ -117,6 +117,7 @@ xpack.alerting.rules.run.ruleTypeOverrides:
- id: siem.indicatorRule
timeout: 1m
xpack.alerting.rules.minimumScheduleInterval.enforce: true
xpack.alerting.rules.maxScheduledPerMinute: 400
xpack.actions.run.maxAttempts: 10
# Disables ESQL in advanced settings (hides it from the UI)

View file

@ -301,6 +301,9 @@ Specifies whether to skip writing alerts and scheduling actions if rule
processing was cancelled due to a timeout. Default: `true`. This setting can be
overridden by individual rule types.
`xpack.alerting.rules.maxScheduledPerMinute` {ess-icon}::
Specifies the maximum number of rules to run per minute. Default: 10000
`xpack.alerting.rules.minimumScheduleInterval.value` {ess-icon}::
Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value.
The time is formatted as a number and a time unit (`s`, `m`, `h`, or `d`).

View file

@ -10,6 +10,7 @@ import {
formatDuration,
getDurationNumberInItsUnit,
getDurationUnitValue,
convertDurationToFrequency,
} from './parse_duration';
test('parses seconds', () => {
@ -180,3 +181,26 @@ test('getDurationUnitValue hours', () => {
const result = getDurationUnitValue('100h');
expect(result).toEqual('h');
});
test('convertDurationToFrequency converts duration', () => {
let result = convertDurationToFrequency('1m');
expect(result).toEqual(1);
result = convertDurationToFrequency('5m');
expect(result).toEqual(0.2);
result = convertDurationToFrequency('10s');
expect(result).toEqual(6);
result = convertDurationToFrequency('1s');
expect(result).toEqual(60);
});
test('convertDurationToFrequency throws when duration is invalid', () => {
expect(() => convertDurationToFrequency('0d')).toThrowErrorMatchingInlineSnapshot(
`"Invalid duration \\"0d\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""`
);
});
test('convertDurationToFrequency throws when denomination is 0', () => {
expect(() => convertDurationToFrequency('1s', 0)).toThrowErrorMatchingInlineSnapshot(
`"Invalid denomination value: value cannot be 0"`
);
});

View file

@ -10,6 +10,8 @@ const MINUTES_REGEX = /^[1-9][0-9]*m$/;
const HOURS_REGEX = /^[1-9][0-9]*h$/;
const DAYS_REGEX = /^[1-9][0-9]*d$/;
const MS_PER_MINUTE = 60 * 1000;
// parse an interval string '{digit*}{s|m|h|d}' into milliseconds
export function parseDuration(duration: string): number {
const parsed = parseInt(duration, 10);
@ -43,6 +45,19 @@ export function formatDuration(duration: string, fullUnit?: boolean): string {
);
}
export function convertDurationToFrequency(
duration: string,
denomination: number = MS_PER_MINUTE
): number {
const durationInMs = parseDuration(duration);
if (denomination === 0) {
throw new Error(`Invalid denomination value: value cannot be 0`);
}
const intervalInDenominationUnits = durationInMs / denomination;
return 1 / intervalInDenominationUnits;
}
export function getDurationNumberInItsUnit(duration: string): number {
return parseInt(duration.replace(/[^0-9.]/g, ''), 10);
}

View file

@ -0,0 +1,26 @@
/*
* 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 {
getScheduleFrequencyResponseSchema,
getScheduleFrequencyResponseBodySchema,
} from './schemas/latest';
export type {
GetScheduleFrequencyResponse,
GetScheduleFrequencyResponseBody,
} from './types/latest';
export {
getScheduleFrequencyResponseSchema as getScheduleFrequencyResponseSchemaV1,
getScheduleFrequencyResponseBodySchema as getScheduleFrequencyResponseBodySchemaV1,
} from './schemas/v1';
export type {
GetScheduleFrequencyResponse as GetScheduleFrequencyResponseV1,
GetScheduleFrequencyResponseBody as GetScheduleFrequencyResponseBodyV1,
} 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

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
export const getScheduleFrequencyResponseBodySchema = schema.object({
total_scheduled_per_minute: schema.number(),
remaining_schedules_per_minute: schema.number(),
});
export const getScheduleFrequencyResponseSchema = schema.object({
body: getScheduleFrequencyResponseBodySchema,
});

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,15 @@
/*
* 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 { getScheduleFrequencyResponseSchemaV1, getScheduleFrequencyResponseBodySchemaV1 } from '..';
export type GetScheduleFrequencyResponseBody = TypeOf<
typeof getScheduleFrequencyResponseBodySchemaV1
>;
export type GetScheduleFrequencyResponse = TypeOf<typeof getScheduleFrequencyResponseSchemaV1>;

View file

@ -10,7 +10,11 @@ import { omit } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -56,7 +60,12 @@ jest.mock('uuid', () => {
return { v4: () => `${uuid++}` };
});
jest.mock('../get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));
const { isSnoozeActive } = jest.requireMock('../../../../lib/snooze/is_snooze_active');
const { validateScheduleLimit } = jest.requireMock('../get_schedule_frequency');
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
@ -65,6 +74,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.2.0';
const createAPIKeyMock = jest.fn();
@ -82,11 +92,13 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: createAPIKeyMock,
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: isAuthenticationTypeApiKeyMock,
getAuthenticationAPIKey: getAuthenticationApiKeyMock,
@ -2495,6 +2507,66 @@ describe('bulkEdit()', () => {
`Error updating rule with ID "${existingDecryptedRule.id}": the interval 10m is longer than the action frequencies`
);
});
test('should only validate schedule limit if schedule is being modified', async () => {
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [
{
id: '1',
type: 'alert',
attributes: {
enabled: true,
tags: ['foo', 'test-1'],
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,
actions: [],
revision: 0,
},
references: [],
version: '123',
},
],
});
await rulesClient.bulkEdit({
filter: '',
operations: [
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
});
expect(validateScheduleLimit).toHaveBeenCalledTimes(0);
await rulesClient.bulkEdit({
operations: [
{
field: 'schedule',
operation: 'set',
value: { interval: '2m' },
},
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
});
expect(validateScheduleLimit).toHaveBeenCalledTimes(1);
});
});
describe('paramsModifier', () => {

View file

@ -77,6 +77,11 @@ import {
transformRuleDomainToRuleAttributes,
transformRuleDomainToRule,
} from '../../transforms';
import { validateScheduleLimit } from '../get_schedule_frequency';
const isValidInterval = (interval: string | undefined): interval is string => {
return interval !== undefined;
};
export const bulkEditFieldsToExcludeFromRevisionUpdates = new Set(['snoozeSchedule', 'apiKey']);
@ -286,8 +291,16 @@ async function bulkEditRulesOcc<Params extends RuleParams>(
const errors: BulkOperationError[] = [];
const apiKeysMap: ApiKeysMap = new Map();
const username = await context.getUserName();
const prevInterval: string[] = [];
for await (const response of rulesFinder.find()) {
const intervals = response.saved_objects
.filter((rule) => rule.attributes.enabled)
.map((rule) => rule.attributes.schedule?.interval)
.filter(isValidInterval);
prevInterval.concat(intervals);
await pMap(
response.saved_objects,
async (rule: SavedObjectsFindResult<RuleAttributes>) =>
@ -308,9 +321,44 @@ async function bulkEditRulesOcc<Params extends RuleParams>(
}
await rulesFinder.close();
const updatedInterval = rules
.filter((rule) => rule.attributes.enabled)
.map((rule) => rule.attributes.schedule?.interval)
.filter(isValidInterval);
try {
if (operations.some((operation) => operation.field === 'schedule')) {
await validateScheduleLimit({
context,
prevInterval,
updatedInterval,
});
}
} catch (error) {
return {
apiKeysToInvalidate: Array.from(apiKeysMap.values())
.filter((value) => value.newApiKey)
.map((value) => value.newApiKey as string),
resultSavedObjects: [],
rules: [],
errors: rules.map((rule) => ({
message: `Failed to bulk edit rule - ${error.message}`,
rule: {
id: rule.id,
name: rule.attributes.name || 'n/a',
},
})),
skipped: [],
};
}
const { result, apiKeysToInvalidate } =
rules.length > 0
? await saveBulkUpdatedRules(context, rules, apiKeysMap)
? await saveBulkUpdatedRules({
context,
rules,
apiKeysMap,
})
: {
result: { saved_objects: [] },
apiKeysToInvalidate: [],
@ -821,11 +869,15 @@ function updateAttributes(
};
}
async function saveBulkUpdatedRules(
context: RulesClientContext,
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>,
apiKeysMap: ApiKeysMap
) {
async function saveBulkUpdatedRules({
context,
rules,
apiKeysMap,
}: {
context: RulesClientContext;
rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>>;
apiKeysMap: ApiKeysMap;
}) {
const apiKeysToInvalidate: string[] = [];
let result;
try {

View file

@ -8,7 +8,11 @@
import { schema } from '@kbn/config-schema';
import { CreateRuleParams } from './create_rule';
import { RulesClient, ConstructorOptions } from '../../../../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -43,6 +47,10 @@ jest.mock('uuid', () => {
return { v4: () => `${uuid++}` };
});
jest.mock('../get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
@ -50,6 +58,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.0.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -63,11 +72,13 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),

View file

@ -36,6 +36,7 @@ import { RuleAttributes } from '../../../../data/rule/types';
import type { CreateRuleData } from './types';
import { createRuleDataSchema } from './schemas';
import { createRuleSavedObject } from '../../../../rules_client/lib';
import { validateScheduleLimit } from '../get_schedule_frequency';
export interface CreateRuleOptions {
id?: string;
@ -60,6 +61,12 @@ export async function createRule<Params extends RuleParams = never>(
try {
createRuleDataSchema.validate(data);
if (data.enabled) {
await validateScheduleLimit({
context,
updatedInterval: data.schedule.interval,
});
}
} catch (error) {
throw Boom.badRequest(`Error validating create data - ${error.message}`);
}

View file

@ -0,0 +1,233 @@
/*
* 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 { validateScheduleLimit } from './get_schedule_frequency';
import { RulesClient, ConstructorOptions } from '../../../../rules_client';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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 { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.0.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
maxScheduledPerMinute: 100,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
const getMockAggregationResult = (
intervalAggs: Array<{
interval: string;
count: number;
}>
) => {
return {
aggregations: {
schedule_intervals: {
buckets: intervalAggs.map(({ interval, count }) => ({
key: interval,
doc_count: count,
})),
},
},
page: 1,
per_page: 20,
total: 1,
saved_objects: [],
};
};
describe('getScheduleFrequency()', () => {
beforeEach(() => {
internalSavedObjectsRepository.find.mockResolvedValue(
getMockAggregationResult([
{ interval: '1m', count: 1 },
{ interval: '1m', count: 2 },
{ interval: '1m', count: 3 },
{ interval: '5m', count: 5 },
{ interval: '5m', count: 10 },
{ interval: '5m', count: 15 },
])
);
});
afterEach(() => {
jest.clearAllMocks();
});
test('should return the correct schedule frequency results', async () => {
const rulesClient = new RulesClient(rulesClientParams);
const result = await rulesClient.getScheduleFrequency();
// (1 * 6) + (1/5 * 30) = 12
expect(result.totalScheduledPerMinute).toEqual(12);
// 100 - 88
expect(result.remainingSchedulesPerMinute).toEqual(88);
});
test('should handle empty bucket correctly', async () => {
internalSavedObjectsRepository.find.mockResolvedValue({
page: 1,
per_page: 20,
total: 1,
saved_objects: [],
});
const rulesClient = new RulesClient(rulesClientParams);
const result = await rulesClient.getScheduleFrequency();
expect(result.totalScheduledPerMinute).toEqual(0);
expect(result.remainingSchedulesPerMinute).toEqual(100);
});
test('should handle malformed schedule interval correctly', async () => {
internalSavedObjectsRepository.find.mockResolvedValue(
getMockAggregationResult([
{ interval: '1m', count: 1 },
{ interval: '1m', count: 2 },
{ interval: '1m', count: 3 },
{ interval: '5m', count: 5 },
{ interval: '5m', count: 10 },
{ interval: '5m', count: 15 },
{ interval: 'invalid', count: 15 },
])
);
const rulesClient = new RulesClient(rulesClientParams);
const result = await rulesClient.getScheduleFrequency();
expect(result.totalScheduledPerMinute).toEqual(12);
expect(result.remainingSchedulesPerMinute).toEqual(88);
});
test('should not go below 0 for remaining schedules', async () => {
internalSavedObjectsRepository.find.mockResolvedValue(
getMockAggregationResult([
{ interval: '1m', count: 1 },
{ interval: '1m', count: 2 },
{ interval: '1m', count: 3 },
{ interval: '5m', count: 5 },
{ interval: '5m', count: 10 },
{ interval: '5m', count: 15 },
])
);
const rulesClient = new RulesClient({
...rulesClientParams,
maxScheduledPerMinute: 10,
});
const result = await rulesClient.getScheduleFrequency();
expect(result.totalScheduledPerMinute).toEqual(12);
expect(result.remainingSchedulesPerMinute).toEqual(0);
});
});
describe('validateScheduleLimit', () => {
const context = {
...rulesClientParams,
maxScheduledPerMinute: 5,
minimumScheduleIntervalInMs: 1000,
fieldsToExcludeFromPublicApi: [],
};
beforeEach(() => {
internalSavedObjectsRepository.find.mockResolvedValue(
getMockAggregationResult([{ interval: '1m', count: 2 }])
);
});
afterEach(() => {
jest.clearAllMocks();
});
test('should not throw if the updated interval does not exceed limits', () => {
return expect(
validateScheduleLimit({
context,
updatedInterval: ['1m', '1m'],
})
).resolves.toBe(undefined);
});
test('should throw if the updated interval exceeds limits', () => {
return expect(
validateScheduleLimit({
context,
updatedInterval: ['1m', '1m', '1m', '2m'],
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Run limit reached: The rule has 3.5 runs per minute; there are only 3 runs per minute available."`
);
});
test('should not throw if previous interval was modified to be under the limit', () => {
internalSavedObjectsRepository.find.mockResolvedValue(
getMockAggregationResult([{ interval: '1m', count: 6 }])
);
return expect(
validateScheduleLimit({
context,
prevInterval: ['1m', '1m'],
updatedInterval: ['2m', '2m'],
})
).resolves.toBe(undefined);
});
test('should throw if the previous interval was modified to exceed the limit', () => {
internalSavedObjectsRepository.find.mockResolvedValue(
getMockAggregationResult([{ interval: '1m', count: 5 }])
);
return expect(
validateScheduleLimit({
context,
prevInterval: ['1m'],
updatedInterval: ['30s'],
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Run limit reached: The rule has 2 runs per minute; there are only 1 runs per minute available."`
);
});
});

View file

@ -0,0 +1,115 @@
/*
* 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 { RulesClientContext } from '../../../../rules_client/types';
import { RuleDomain } from '../../types';
import { convertDurationToFrequency } from '../../../../../common/parse_duration';
import { GetScheduleFrequencyResult } from './types';
import { getSchemaFrequencyResultSchema } from './schema';
export interface SchedulesIntervalAggregationResult {
schedule_intervals: {
buckets: Array<{
key: string;
doc_count: number;
}>;
};
}
const convertIntervalToFrequency = (context: RulesClientContext, schedule: string) => {
let scheduleFrequency = 0;
try {
// Normalize the interval (period) in terms of minutes
scheduleFrequency = convertDurationToFrequency(schedule);
} catch (e) {
context.logger.warn(
`Failed to parse rule schedule interval for schedule frequency calculation: ${e.message}`
);
}
return scheduleFrequency;
};
export const getScheduleFrequency = async (
context: RulesClientContext
): Promise<GetScheduleFrequencyResult> => {
const response = await context.internalSavedObjectsRepository.find<
RuleDomain,
SchedulesIntervalAggregationResult
>({
type: 'alert',
filter: 'alert.attributes.enabled: true',
namespaces: ['*'],
aggs: {
schedule_intervals: {
terms: {
field: 'alert.attributes.schedule.interval',
},
},
},
});
const buckets = response.aggregations?.schedule_intervals.buckets ?? [];
const totalScheduledPerMinute = buckets.reduce((result, { key, doc_count: occurrence }) => {
const scheduleFrequency = convertIntervalToFrequency(context, key);
// Sum up all of the frequencies, since this is an aggregation.
return result + scheduleFrequency * occurrence;
}, 0);
const result = {
totalScheduledPerMinute,
remainingSchedulesPerMinute: Math.max(
context.maxScheduledPerMinute - totalScheduledPerMinute,
0
),
};
try {
getSchemaFrequencyResultSchema.validate(result);
} catch (e) {
context.logger.warn(`Error validating rule schedules per minute: ${e}`);
}
return result;
};
interface ValidateScheduleLimitParams {
context: RulesClientContext;
prevInterval?: string | string[];
updatedInterval: string | string[];
}
export const validateScheduleLimit = async (params: ValidateScheduleLimitParams) => {
const { context, prevInterval = [], updatedInterval = [] } = params;
const prevIntervalArray = Array.isArray(prevInterval) ? prevInterval : [prevInterval];
const updatedIntervalArray = Array.isArray(updatedInterval) ? updatedInterval : [updatedInterval];
const prevSchedulePerMinute = prevIntervalArray.reduce((result, interval) => {
const scheduleFrequency = convertIntervalToFrequency(context, interval);
return result + scheduleFrequency;
}, 0);
const updatedSchedulesPerMinute = updatedIntervalArray.reduce((result, interval) => {
const scheduleFrequency = convertIntervalToFrequency(context, interval);
return result + scheduleFrequency;
}, 0);
const { remainingSchedulesPerMinute } = await getScheduleFrequency(context);
// Compute the new remaining schedules per minute if we are editing rules.
// So we add back the edited schedules, since we assume those are being edited.
const computedRemainingSchedulesPerMinute = remainingSchedulesPerMinute + prevSchedulePerMinute;
if (computedRemainingSchedulesPerMinute < updatedSchedulesPerMinute) {
throw new Error(
`Run limit reached: The rule has ${updatedSchedulesPerMinute} runs per minute; there are only ${computedRemainingSchedulesPerMinute} runs per minute available.`
);
}
};

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 type { GetScheduleFrequencyResult } from './types';
export { getScheduleFrequency, validateScheduleLimit } from './get_schedule_frequency';

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.
*/
import { schema } from '@kbn/config-schema';
export const getSchemaFrequencyResultSchema = schema.object({
totalScheduledPerMinute: schema.number({ min: 0 }),
remainingSchedulesPerMinute: schema.number({ min: 0 }),
});

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 { getSchemaFrequencyResultSchema } from './get_schedule_frequency_result_schema';

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 { getSchemaFrequencyResultSchema } from '../schema';
export type GetScheduleFrequencyResult = TypeOf<typeof getSchemaFrequencyResultSchema>;

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 type { GetScheduleFrequencyResult } from './get_schedule_frequency_result';

View file

@ -23,6 +23,7 @@ describe('config validation', () => {
},
"maxEphemeralActionsPerAlert": 10,
"rules": Object {
"maxScheduledPerMinute": 10000,
"minimumScheduleInterval": Object {
"enforce": false,
"value": "1m",

View file

@ -38,6 +38,7 @@ const rulesSchema = schema.object({
}),
enforce: schema.boolean({ defaultValue: false }), // if enforce is false, only warnings will be shown
}),
maxScheduledPerMinute: schema.number({ defaultValue: 10000, max: 10000, min: 0 }),
run: schema.object({
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
actions: schema.object({
@ -70,7 +71,10 @@ export const configSchema = schema.object({
export type AlertingConfig = TypeOf<typeof configSchema>;
export type RulesConfig = TypeOf<typeof rulesSchema>;
export type AlertingRulesConfig = Pick<AlertingConfig['rules'], 'minimumScheduleInterval'> & {
export type AlertingRulesConfig = Pick<
AlertingConfig['rules'],
'minimumScheduleInterval' | 'maxScheduledPerMinute'
> & {
isUsingSecurity: boolean;
};
export type ActionsConfig = RulesConfig['run']['actions'];

View file

@ -139,6 +139,7 @@ describe('Alerting Plugin', () => {
await waitForSetupComplete(setupMocks);
expect(setupContract.getConfig()).toEqual({
maxScheduledPerMinute: 10000,
isUsingSecurity: false,
minimumScheduleInterval: { value: '1m', enforce: false },
});

View file

@ -415,7 +415,7 @@ export class AlertingPlugin {
},
getConfig: () => {
return {
...pick(this.config.rules, 'minimumScheduleInterval'),
...pick(this.config.rules, ['minimumScheduleInterval', 'maxScheduledPerMinute']),
isUsingSecurity: this.licenseState ? !!this.licenseState.getIsSecurityEnabled() : false,
};
},
@ -481,6 +481,7 @@ export class AlertingPlugin {
taskManager: plugins.taskManager,
securityPluginSetup: security,
securityPluginStart: plugins.security,
internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']),
encryptedSavedObjectsClient,
spaceIdToNamespace,
getSpaceId(request: KibanaRequest) {
@ -492,6 +493,7 @@ export class AlertingPlugin {
authorization: alertingAuthorizationClientFactory,
eventLogger: this.eventLogger,
minimumScheduleInterval: this.config.rules.minimumScheduleInterval,
maxScheduledPerMinute: this.config.rules.maxScheduledPerMinute,
});
rulesSettingsClientFactory.initialize({

View file

@ -47,6 +47,7 @@ import { cloneRuleRoute } from './clone_rule';
import { getFlappingSettingsRoute } from './get_flapping_settings';
import { updateFlappingSettingsRoute } from './update_flapping_settings';
import { getRuleTagsRoute } from './get_rule_tags';
import { getScheduleFrequencyRoute } from './rule/apis/get_schedule_frequency';
import { createMaintenanceWindowRoute } from './maintenance_window/create_maintenance_window';
import { getMaintenanceWindowRoute } from './maintenance_window/get_maintenance_window';
@ -129,4 +130,5 @@ export function defineRoutes(opts: RouteOptions) {
registerRulesValueSuggestionsRoute(router, licenseState, config$!);
registerFieldsRoute(router, licenseState);
bulkGetMaintenanceWindowRoute(router, licenseState);
getScheduleFrequencyRoute(router, licenseState);
}

View file

@ -0,0 +1,59 @@
/*
* 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 { getScheduleFrequencyRoute } from './get_schedule_frequency_route';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../../../../lib/license_state.mock';
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
import { rulesClientMock } from '../../../../rules_client.mock';
const rulesClient = rulesClientMock.create();
jest.mock('../../../../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
});
describe('getScheduleFrequencyRoute', () => {
it('gets the schedule frequency limit and remaining allotment', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getScheduleFrequencyRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toBe('/internal/alerting/rules/_schedule_frequency');
expect(config).toMatchInlineSnapshot(`
Object {
"path": "/internal/alerting/rules/_schedule_frequency",
"validate": Object {},
}
`);
rulesClient.getScheduleFrequency.mockResolvedValueOnce({
totalScheduledPerMinute: 9000,
remainingSchedulesPerMinute: 1000,
});
const [context, req, res] = mockHandlerArguments({ rulesClient }, {}, ['ok']);
await handler(context, req, res);
expect(rulesClient.getScheduleFrequency).toHaveBeenCalledTimes(1);
expect(res.ok).toHaveBeenCalledWith({
body: {
total_scheduled_per_minute: 9000,
remaining_schedules_per_minute: 1000,
},
});
});
});

View file

@ -0,0 +1,38 @@
/*
* 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 } from '../../../../lib';
import { verifyAccessAndContext } from '../../../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types';
import { GetScheduleFrequencyResponseV1 } from '../../../../../common/routes/rule/apis/get_schedule_frequency';
import { transformGetScheduleFrequencyResultV1 } from './transforms';
export const getScheduleFrequencyRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_schedule_frequency`,
validate: {},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async (context, req, res) => {
const rulesClient = (await context.alerting).getRulesClient();
const scheduleFrequencyResult = await rulesClient.getScheduleFrequency();
const response: GetScheduleFrequencyResponseV1 = {
body: transformGetScheduleFrequencyResultV1(scheduleFrequencyResult),
};
return res.ok(response);
})
)
);
};

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 { getScheduleFrequencyRoute } from './get_schedule_frequency_route';

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 { transformGetScheduleFrequencyResult } from './transform_get_schedule_frequency_result/latest';
export { transformGetScheduleFrequencyResult as transformGetScheduleFrequencyResultV1 } from './transform_get_schedule_frequency_result/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 type { GetScheduleFrequencyResponseBodyV1 } from '../../../../../../../common/routes/rule/apis/get_schedule_frequency';
import type { GetScheduleFrequencyResult } from '../../../../../../application/rule/methods/get_schedule_frequency';
export const transformGetScheduleFrequencyResult = (
result: GetScheduleFrequencyResult
): GetScheduleFrequencyResponseBodyV1 => {
return {
total_scheduled_per_minute: result.totalScheduledPerMinute,
remaining_schedules_per_minute: result.remainingSchedulesPerMinute,
};
};

View file

@ -52,6 +52,7 @@ const createRulesClientMock = () => {
runSoon: jest.fn(),
clone: jest.fn(),
getAlertFromRaw: jest.fn(),
getScheduleFrequency: jest.fn(),
};
return mocked;
};

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -24,6 +28,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.0.0';
const rulesClientParams: jest.Mocked<RulesClientContext> = {
@ -36,10 +41,12 @@ const rulesClientParams: jest.Mocked<RulesClientContext> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
minimumScheduleIntervalInMs: 1,
fieldsToExcludeFromPublicApi: [],

View file

@ -4,9 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import pMap from 'p-map';
import { KueryNode, nodeBuilder } from '@kbn/es-query';
import { SavedObjectsBulkUpdateObject } from '@kbn/core/server';
import { SavedObjectsBulkUpdateObject, SavedObjectsFindResult } from '@kbn/core/server';
import { withSpan } from '@kbn/apm-utils';
import { Logger } from '@kbn/core/server';
import { TaskManagerStartContract, TaskStatus } from '@kbn/task-manager-plugin/server';
@ -28,6 +29,7 @@ import {
migrateLegacyActions,
} from '../lib';
import { RulesClientContext, BulkOperationError, BulkOptions } from '../types';
import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency';
const getShouldScheduleTask = async (
context: RulesClientContext,
@ -121,116 +123,136 @@ const bulkEnableRulesWithOCC = async (
)
);
const rulesFinderRules: Array<SavedObjectsFindResult<RawRule>> = [];
const rulesToEnable: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
const errors: BulkOperationError[] = [];
const ruleNameToRuleIdMapping: Record<string, string> = {};
const username = await context.getUserName();
let scheduleValidationError = '';
await withSpan(
{ name: 'Get rules, collect them and their attributes', type: 'rules' },
async () => {
for await (const response of rulesFinder.find()) {
await pMap(response.saved_objects, async (rule) => {
try {
if (rule.attributes.actions.length) {
try {
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
} catch (error) {
throw Error(`Rule not authorized for bulk enable - ${error.message}`);
}
}
if (rule.attributes.name) {
ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
}
const migratedActions = await migrateLegacyActions(context, {
ruleId: rule.id,
actions: rule.attributes.actions,
references: rule.references,
attributes: rule.attributes,
});
const updatedAttributes = updateMeta(context, {
...rule.attributes,
...(!rule.attributes.apiKey &&
(await createNewAPIKeySet(context, {
id: rule.attributes.alertTypeId,
ruleName: rule.attributes.name,
username,
shouldUpdateApiKey: true,
}))),
...(migratedActions.hasLegacyActions
? {
actions: migratedActions.resultedActions,
throttle: undefined,
notifyWhen: undefined,
}
: {}),
enabled: true,
updatedBy: username,
updatedAt: new Date().toISOString(),
executionStatus: {
status: 'pending',
lastDuration: 0,
lastExecutionDate: new Date().toISOString(),
error: null,
warning: null,
},
});
const shouldScheduleTask = await getShouldScheduleTask(
context,
rule.attributes.scheduledTaskId
);
let scheduledTaskId;
if (shouldScheduleTask) {
const scheduledTask = await scheduleTask(context, {
id: rule.id,
consumer: rule.attributes.consumer,
ruleTypeId: rule.attributes.alertTypeId,
schedule: rule.attributes.schedule as IntervalSchedule,
throwOnConflict: false,
});
scheduledTaskId = scheduledTask.id;
}
rulesToEnable.push({
...rule,
attributes: {
...updatedAttributes,
...(scheduledTaskId ? { scheduledTaskId } : undefined),
},
...(migratedActions.hasLegacyActions
? { references: migratedActions.resultedReferences }
: {}),
});
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.ENABLE,
outcome: 'unknown',
savedObject: { type: 'alert', id: rule.id },
})
);
} catch (error) {
errors.push({
message: error.message,
rule: {
id: rule.id,
name: rule.attributes?.name,
},
});
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.ENABLE,
error,
})
);
}
});
rulesFinderRules.push(...response.saved_objects);
}
await rulesFinder.close();
const updatedInterval = rulesFinderRules
.filter((rule) => !rule.attributes.enabled)
.map((rule) => rule.attributes.schedule?.interval);
try {
await validateScheduleLimit({
context,
updatedInterval,
});
} catch (error) {
scheduleValidationError = `Error validating enable rule data - ${error.message}`;
}
await pMap(rulesFinderRules, async (rule) => {
try {
if (scheduleValidationError) {
throw Error(scheduleValidationError);
}
if (rule.attributes.actions.length) {
try {
await context.actionsAuthorization.ensureAuthorized({ operation: 'execute' });
} catch (error) {
throw Error(`Rule not authorized for bulk enable - ${error.message}`);
}
}
if (rule.attributes.name) {
ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
}
const migratedActions = await migrateLegacyActions(context, {
ruleId: rule.id,
actions: rule.attributes.actions,
references: rule.references,
attributes: rule.attributes,
});
const updatedAttributes = updateMeta(context, {
...rule.attributes,
...(!rule.attributes.apiKey &&
(await createNewAPIKeySet(context, {
id: rule.attributes.alertTypeId,
ruleName: rule.attributes.name,
username,
shouldUpdateApiKey: true,
}))),
...(migratedActions.hasLegacyActions
? {
actions: migratedActions.resultedActions,
throttle: undefined,
notifyWhen: undefined,
}
: {}),
enabled: true,
updatedBy: username,
updatedAt: new Date().toISOString(),
executionStatus: {
status: 'pending',
lastDuration: 0,
lastExecutionDate: new Date().toISOString(),
error: null,
warning: null,
},
});
const shouldScheduleTask = await getShouldScheduleTask(
context,
rule.attributes.scheduledTaskId
);
let scheduledTaskId;
if (shouldScheduleTask) {
const scheduledTask = await scheduleTask(context, {
id: rule.id,
consumer: rule.attributes.consumer,
ruleTypeId: rule.attributes.alertTypeId,
schedule: rule.attributes.schedule as IntervalSchedule,
throwOnConflict: false,
});
scheduledTaskId = scheduledTask.id;
}
rulesToEnable.push({
...rule,
attributes: {
...updatedAttributes,
...(scheduledTaskId ? { scheduledTaskId } : undefined),
},
...(migratedActions.hasLegacyActions
? { references: migratedActions.resultedReferences }
: {}),
});
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.ENABLE,
outcome: 'unknown',
savedObject: { type: 'alert', id: rule.id },
})
);
} catch (error) {
errors.push({
message: error.message,
rule: {
id: rule.id,
name: rule.attributes?.name,
},
});
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.ENABLE,
error,
})
);
}
});
}
);

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Boom from '@hapi/boom';
import type { SavedObjectReference } from '@kbn/core/server';
import { TaskStatus } from '@kbn/task-manager-plugin/server';
import { RawRule, IntervalSchedule } from '../../types';
@ -13,6 +14,7 @@ import { retryIfConflicts } from '../../lib/retry_if_conflicts';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { RulesClientContext } from '../types';
import { updateMeta, createNewAPIKeySet, scheduleTask, migrateLegacyActions } from '../lib';
import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency';
export async function enable(context: RulesClientContext, { id }: { id: string }): Promise<void> {
return await retryIfConflicts(
@ -46,6 +48,15 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
references = alert.references;
}
try {
await validateScheduleLimit({
context,
updatedInterval: attributes.schedule.interval,
});
} catch (error) {
throw Boom.badRequest(`Error validating enable rule data - ${error.message}`);
}
try {
await context.authorization.ensureAuthorized({
ruleTypeId: attributes.alertTypeId,

View file

@ -33,6 +33,7 @@ import {
createNewAPIKeySet,
migrateLegacyActions,
} from '../lib';
import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency';
type ShouldIncrementRevision = (params?: RuleTypeParams) => boolean;
@ -88,6 +89,21 @@ async function updateWithOCC<Params extends RuleTypeParams>(
alertSavedObject = await context.unsecuredSavedObjectsClient.get<RawRule>('alert', id);
}
const {
attributes: { enabled, schedule },
} = alertSavedObject;
try {
if (enabled && schedule.interval !== data.schedule.interval) {
await validateScheduleLimit({
context,
prevInterval: alertSavedObject.attributes.schedule?.interval,
updatedInterval: data.schedule.interval,
});
}
} catch (error) {
throw Boom.badRequest(`Error validating update data - ${error.message}`);
}
try {
await context.authorization.ensureAuthorized({
ruleTypeId: alertSavedObject.attributes.alertTypeId,

View file

@ -57,6 +57,7 @@ import { runSoon } from './methods/run_soon';
import { listRuleTypes } from './methods/list_rule_types';
import { getAlertFromRaw, GetAlertFromRawParams } from './lib/get_alert_from_raw';
import { getTags, GetTagsParams } from './methods/get_tags';
import { getScheduleFrequency } from '../application/rule/methods/get_schedule_frequency/get_schedule_frequency';
export type ConstructorOptions = Omit<
RulesClientContext,
@ -179,6 +180,8 @@ export class RulesClient {
public getTags = (params: GetTagsParams) => getTags(this.context, params);
public getScheduleFrequency = () => getScheduleFrequency(this.context);
public getAlertFromRaw = (params: GetAlertFromRawParams) =>
getAlertFromRaw(
this.context,

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -32,12 +36,14 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
@ -46,6 +52,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,7 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { savedObjectsClientMock, savedObjectsRepositoryMock } 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';
@ -53,6 +53,7 @@ const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const logger = loggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.2.0';
const createAPIKeyMock = jest.fn();
@ -67,11 +68,13 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: createAPIKeyMock,
logger,
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),

View file

@ -6,7 +6,7 @@
*/
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { savedObjectsClientMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import type { SavedObject } from '@kbn/core-saved-objects-server';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
@ -61,6 +61,7 @@ const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const logger = loggerMock.create();
const eventLogger = eventLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.2.0';
const createAPIKeyMock = jest.fn();
@ -75,12 +76,14 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: createAPIKeyMock,
logger,
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
eventLogger,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),

View file

@ -6,7 +6,7 @@
*/
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { savedObjectsClientMock, savedObjectsRepositoryMock } 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';
@ -45,6 +45,10 @@ jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation
bulkMarkApiKeysForInvalidation: jest.fn(),
}));
jest.mock('../../application/rule/methods/get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
@ -53,6 +57,7 @@ const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const logger = loggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v8.2.0';
const createAPIKeyMock = jest.fn();
@ -67,11 +72,13 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: createAPIKeyMock,
logger,
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),

View file

@ -8,7 +8,11 @@
import moment from 'moment';
import sinon from 'sinon';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -40,6 +44,7 @@ const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const eventLogger = eventLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -50,10 +55,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -8,7 +8,11 @@
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -43,12 +47,14 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
@ -57,6 +63,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -44,6 +48,7 @@ const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const eventLogger = eventLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -54,10 +59,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -31,6 +35,10 @@ jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation
bulkMarkApiKeysForInvalidation: jest.fn(),
}));
jest.mock('../../application/rule/methods/get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
@ -38,6 +46,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -48,10 +57,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -36,6 +40,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -46,10 +51,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -33,6 +37,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -43,10 +48,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import { RulesClient, ConstructorOptions } from '../rules_client';
import { GetActionErrorLogByIdParams } from '../methods/get_action_error_log';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { fromKueryExpression } from '@kbn/es-query';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
@ -31,6 +35,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -41,10 +46,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -24,6 +28,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -34,10 +39,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import { omit, mean } from 'lodash';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -30,6 +34,7 @@ const eventLogClient = eventLogClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -40,10 +45,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -32,6 +36,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -42,10 +47,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { v4 } from 'uuid';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -27,12 +31,14 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
@ -41,6 +47,7 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -28,6 +32,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -38,10 +43,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -24,6 +28,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -34,10 +39,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -24,6 +28,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -34,10 +39,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -7,7 +7,11 @@
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -33,6 +37,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -43,10 +48,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -25,6 +29,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -35,10 +40,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -24,6 +28,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -34,10 +39,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -24,6 +28,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -34,10 +39,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -9,7 +9,11 @@ import { v4 as uuidv4 } from 'uuid';
import { schema } from '@kbn/config-schema';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -50,6 +54,10 @@ jest.mock('uuid', () => {
return { v4: () => `${uuid++}` };
});
jest.mock('../../application/rule/methods/get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));
const bulkMarkApiKeysForInvalidationMock = bulkMarkApiKeysForInvalidation as jest.Mock;
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
@ -58,6 +66,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -71,11 +80,13 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),

View file

@ -6,7 +6,11 @@
*/
import { RulesClient, ConstructorOptions } from '../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -30,6 +34,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
@ -40,10 +45,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
namespace: 'default',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),

View file

@ -6,7 +6,12 @@
*/
import { KueryNode } from '@kbn/es-query';
import { Logger, SavedObjectsClientContract, PluginInitializerContext } from '@kbn/core/server';
import {
Logger,
SavedObjectsClientContract,
PluginInitializerContext,
ISavedObjectsRepository,
} from '@kbn/core/server';
import { ActionsClient, ActionsAuthorization } from '@kbn/actions-plugin/server';
import {
GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult,
@ -55,11 +60,13 @@ export interface RulesClientContext {
readonly authorization: AlertingAuthorization;
readonly ruleTypeRegistry: RuleTypeRegistry;
readonly minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
readonly maxScheduledPerMinute: AlertingRulesConfig['maxScheduledPerMinute'];
readonly minimumScheduleIntervalInMs: number;
readonly createAPIKey: (name: string) => Promise<CreateAPIKeyResult>;
readonly getActionsClient: () => Promise<ActionsClient>;
readonly actionsAuthorization: ActionsAuthorization;
readonly getEventLogClient: () => Promise<IEventLogClient>;
readonly internalSavedObjectsRepository: ISavedObjectsRepository;
readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
readonly auditLogger?: AuditLogger;

View file

@ -8,7 +8,11 @@
import { cloneDeep } from 'lodash';
import { RulesClient, ConstructorOptions } from './rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} 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';
@ -21,6 +25,10 @@ import { RetryForConflictsAttempts } from './lib/retry_if_conflicts';
import { TaskStatus } from '@kbn/task-manager-plugin/server/task';
import { RecoveredActionGroup } from '../common';
jest.mock('./application/rule/methods/get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));
let rulesClient: RulesClient;
const MockAlertId = 'alert-id';
@ -34,6 +42,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const kibanaVersion = 'v7.10.0';
const logger = loggingSystemMock.create().get();
@ -48,10 +57,12 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger,
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),

View file

@ -12,6 +12,7 @@ import {
savedObjectsClientMock,
savedObjectsServiceMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} from '@kbn/core/server/mocks';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { AuthenticatedUser } from '@kbn/security-plugin/common/model';
@ -37,6 +38,7 @@ const securityPluginStart = securityMock.createStart();
const alertingAuthorization = alertingAuthorizationMock.create();
const alertingAuthorizationClientFactory = alertingAuthorizationClientFactoryMock.createFactory();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();
const rulesClientFactoryParams: jest.Mocked<RulesClientFactoryOpts> = {
logger: loggingSystemMock.create().get(),
@ -44,7 +46,9 @@ const rulesClientFactoryParams: jest.Mocked<RulesClientFactoryOpts> = {
ruleTypeRegistry: ruleTypeRegistryMock.create(),
getSpaceId: jest.fn(),
spaceIdToNamespace: jest.fn(),
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(),
actions: actionsMock.createStart(),
eventLog: eventLogMock.createStart(),
@ -101,8 +105,10 @@ test('creates a rules client with proper constructor arguments when security is
getActionsClient: expect.any(Function),
getEventLogClient: expect.any(Function),
createAPIKey: expect.any(Function),
internalSavedObjectsRepository: rulesClientFactoryParams.internalSavedObjectsRepository,
encryptedSavedObjectsClient: rulesClientFactoryParams.encryptedSavedObjectsClient,
kibanaVersion: '7.10.0',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: expect.any(Function),
getAuthenticationAPIKey: expect.any(Function),
@ -139,10 +145,12 @@ test('creates a rules client with proper constructor arguments', async () => {
namespace: 'default',
getUserName: expect.any(Function),
createAPIKey: expect.any(Function),
internalSavedObjectsRepository: rulesClientFactoryParams.internalSavedObjectsRepository,
encryptedSavedObjectsClient: rulesClientFactoryParams.encryptedSavedObjectsClient,
getActionsClient: expect.any(Function),
getEventLogClient: expect.any(Function),
kibanaVersion: '7.10.0',
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: expect.any(Function),
getAuthenticationAPIKey: expect.any(Function),

View file

@ -10,6 +10,7 @@ import {
Logger,
SavedObjectsServiceStart,
PluginInitializerContext,
ISavedObjectsRepository,
} from '@kbn/core/server';
import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
import {
@ -34,12 +35,14 @@ export interface RulesClientFactoryOpts {
getSpaceId: (request: KibanaRequest) => string;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
internalSavedObjectsRepository: ISavedObjectsRepository;
actions: ActionsPluginStartContract;
eventLog: IEventLogClientService;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
authorization: AlertingAuthorizationClientFactory;
eventLogger?: IEventLogger;
minimumScheduleInterval: AlertingRulesConfig['minimumScheduleInterval'];
maxScheduledPerMinute: AlertingRulesConfig['maxScheduledPerMinute'];
}
export class RulesClientFactory {
@ -52,12 +55,14 @@ export class RulesClientFactory {
private getSpaceId!: (request: KibanaRequest) => string;
private spaceIdToNamespace!: SpaceIdToNamespaceFunction;
private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient;
private internalSavedObjectsRepository!: ISavedObjectsRepository;
private actions!: ActionsPluginStartContract;
private eventLog!: IEventLogClientService;
private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version'];
private authorization!: AlertingAuthorizationClientFactory;
private eventLogger?: IEventLogger;
private minimumScheduleInterval!: AlertingRulesConfig['minimumScheduleInterval'];
private maxScheduledPerMinute!: AlertingRulesConfig['maxScheduledPerMinute'];
public initialize(options: RulesClientFactoryOpts) {
if (this.isInitialized) {
@ -72,12 +77,14 @@ export class RulesClientFactory {
this.securityPluginStart = options.securityPluginStart;
this.spaceIdToNamespace = options.spaceIdToNamespace;
this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient;
this.internalSavedObjectsRepository = options.internalSavedObjectsRepository;
this.actions = options.actions;
this.eventLog = options.eventLog;
this.kibanaVersion = options.kibanaVersion;
this.authorization = options.authorization;
this.eventLogger = options.eventLogger;
this.minimumScheduleInterval = options.minimumScheduleInterval;
this.maxScheduledPerMinute = options.maxScheduledPerMinute;
}
public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): RulesClient {
@ -95,6 +102,7 @@ export class RulesClientFactory {
taskManager: this.taskManager,
ruleTypeRegistry: this.ruleTypeRegistry,
minimumScheduleInterval: this.minimumScheduleInterval,
maxScheduledPerMinute: this.maxScheduledPerMinute,
unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, {
excludedExtensions: [SECURITY_EXTENSION_ID],
includedHiddenTypes: ['alert', 'api_key_pending_invalidation'],
@ -102,6 +110,7 @@ export class RulesClientFactory {
authorization: this.authorization.create(request),
actionsAuthorization: actions.getActionsAuthorizationWithRequest(request),
namespace: this.spaceIdToNamespace(spaceId),
internalSavedObjectsRepository: this.internalSavedObjectsRepository,
encryptedSavedObjectsClient: this.encryptedSavedObjectsClient,
auditLogger: securityPluginSetup?.audit.asScoped(request),
async getUserName() {

View file

@ -60,6 +60,7 @@ export function generateAlertingConfig(): AlertingConfig {
maxEphemeralActionsPerAlert: 10,
cancelAlertsOnRuleTimeout: true,
rules: {
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
run: {
actions: {

View file

@ -51,6 +51,7 @@ describe('createConfigRoute', () => {
baseRoute: `/internal/triggers_actions_ui`,
alertingConfig: () => ({
isUsingSecurity: true,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
}),
getRulesClientWithRequest: () => mockRulesClient,
@ -64,7 +65,11 @@ describe('createConfigRoute', () => {
expect(mockResponse.ok).toBeCalled();
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
body: { isUsingSecurity: true, minimumScheduleInterval: { value: '1m', enforce: false } },
body: {
isUsingSecurity: true,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
},
});
});
@ -80,6 +85,7 @@ describe('createConfigRoute', () => {
baseRoute: `/internal/triggers_actions_ui`,
alertingConfig: () => ({
isUsingSecurity: true,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
}),
getRulesClientWithRequest: () => mockRulesClient,

View file

@ -28,6 +28,7 @@ interface CreateTestConfigOptions {
reportName?: string;
useDedicatedTaskRunner: boolean;
enableFooterInEmail?: boolean;
maxScheduledPerMinute?: number;
}
// test.not-enabled is specifically not enabled
@ -82,6 +83,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
reportName = undefined,
useDedicatedTaskRunner,
enableFooterInEmail = true,
maxScheduledPerMinute,
} = options;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
@ -151,6 +153,11 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
? [`--xpack.actions.email.domain_allowlist=${JSON.stringify(emailDomainsAllowed)}`]
: [];
const maxScheduledPerMinuteSettings =
typeof maxScheduledPerMinute === 'number'
? [`--xpack.alerting.rules.maxScheduledPerMinute=${maxScheduledPerMinute}`]
: [];
return {
testFiles: testFiles ? testFiles : [require.resolve(`../${name}/tests/`)],
servers,
@ -199,6 +206,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...actionsProxyUrl,
...customHostSettings,
...emailSettings,
...maxScheduledPerMinuteSettings,
'--xpack.eventLog.logEntries=true',
'--xpack.task_manager.ephemeral_tasks.enabled=false',
`--xpack.task_manager.unsafe.exclude_task_types=${JSON.stringify([

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.
*/
import { createTestConfig } from '../../common/config';
// eslint-disable-next-line import/no-default-export
export default createTestConfig('security_and_spaces', {
disabledPlugins: [],
license: 'trial',
ssl: true,
enableActionsProxy: true,
publicBaseUrl: true,
testFiles: [require.resolve('./tests/alerting/schedule_circuit_breaker')],
useDedicatedTaskRunner: true,
maxScheduledPerMinute: 10,
});

View file

@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib';
// eslint-disable-next-line import/no-default-export
export default function bulkEditWithCircuitBreakerTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
describe('Bulk edit with circuit breaker', () => {
afterEach(async () => {
await objectRemover.removeAll();
});
it('should prevent rules from being bulk edited if max schedules have been reached', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const { body: createdRule3 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule3.id, 'rule', 'alerting');
const payload = {
ids: [createdRule2.id, createdRule3.id],
operations: [
{
operation: 'set',
field: 'schedule',
value: {
interval: '10s',
},
},
],
};
const { body } = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/_bulk_edit`)
.set('kbn-xsrf', 'foo')
.send(payload)
.expect(200);
expect(body.errors.length).eql(2);
expect(body.errors[0].message).eql(
'Failed to bulk edit rule - Run limit reached: The rule has 12 runs per minute; there are only 1 runs per minute available.'
);
});
it('should allow disabled rules to go over the circuit breaker', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '20s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const { body: createdRule3 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '20s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule3.id, 'rule', 'alerting');
const payload = {
ids: [createdRule2.id, createdRule3.id],
operations: [
{
operation: 'set',
field: 'schedule',
value: {
interval: '10s',
},
},
],
};
const { body } = await supertest
.post(`${getUrlPrefix('space1')}/internal/alerting/rules/_bulk_edit`)
.set('kbn-xsrf', 'foo')
.send(payload)
.expect(200);
expect(body.rules.length).eql(2);
expect(body.errors.length).eql(0);
});
});
}

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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib';
// eslint-disable-next-line import/no-default-export
export default function bulkEnableWithCircuitBreakerTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
describe('Bulk enable with circuit breaker', () => {
afterEach(async () => {
await objectRemover.removeAll();
});
it('should prevent rules from being bulk enabled if max schedules have been reached', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '20s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const { body: createdRule3 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '10s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule3.id, 'rule', 'alerting');
const { body } = await supertest
.patch(`${getUrlPrefix('space1')}/internal/alerting/rules/_bulk_enable`)
.set('kbn-xsrf', 'foo')
.send({ ids: [createdRule2.id, createdRule3.id] })
.expect(200);
expect(body.errors.length).eql(2);
expect(body.errors[0].message).eql(
'Error validating enable rule data - Run limit reached: The rule has 9 runs per minute; there are only 4 runs per minute available.'
);
});
});
}

View file

@ -0,0 +1,73 @@
/*
* 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib';
// eslint-disable-next-line import/no-default-export
export default function createWithCircuitBreakerTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
describe('Create with circuit breaker', () => {
afterEach(async () => {
await objectRemover.removeAll();
});
it('should prevent rules from being created if max schedules have been reached', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add('space1', createdRule.id, 'rule', 'alerting');
const { body } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(400);
});
it('should prevent rules from being created across spaces', async () => {
const { body: createdRule } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add('space1', createdRule.id, 'rule', 'alerting');
const { body } = await supertest
.post(`${getUrlPrefix('space2')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(400);
});
it('should allow disabled rules to go over the circuit breaker', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '10s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
});
});
}

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib';
// eslint-disable-next-line import/no-default-export
export default function enableWithCircuitBreakerTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
describe('Enable with circuit breaker', () => {
afterEach(async () => {
await objectRemover.removeAll();
});
it('should prevent rules from being enabled if max schedules have been reached', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '10s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '5s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const { body } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule/${createdRule2.id}/_enable`)
.set('kbn-xsrf', 'foo')
.expect(400);
expect(body.message).eql(
'Error validating enable rule data - Run limit reached: The rule has 12 runs per minute; there are only 4 runs per minute available.'
);
});
});
}

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { UserAtSpaceScenarios } from '../../../../scenarios';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib';
// eslint-disable-next-line import/no-default-export
export default function getScheduleFrequencyTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const objectRemover = new ObjectRemover(supertest);
describe('getScheduleFrequency', () => {
before(async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '30s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '1m' } }))
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const { body: createdRule3 } = await supertest
.post(`${getUrlPrefix('space2')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '2m' } }))
.expect(200);
objectRemover.add('space2', createdRule3.id, 'rule', 'alerting');
const { body: createdRule4 } = await supertest
.post(`${getUrlPrefix('space2')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '30s' } }))
.expect(200);
objectRemover.add('space2', createdRule4.id, 'rule', 'alerting');
});
after(async () => {
await objectRemover.removeAll();
});
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should get the total and remaining schedule frequency', async () => {
const { body } = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/_schedule_frequency`)
.set('kbn-xsrf', 'foo')
.send()
.auth(user.username, user.password);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(body.total_scheduled_per_minute).eql(5.5);
expect(body.remaining_schedules_per_minute).eql(4.5);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});
}

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 { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { setupSpacesAndUsers, tearDown } from '../../../../setup';
// eslint-disable-next-line import/no-default-export
export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) {
describe('Alerts - Group 3 - schedule circuit breaker', () => {
describe('alerts', () => {
before(async () => {
await setupSpacesAndUsers(getService);
});
after(async () => {
await tearDown(getService);
});
loadTestFile(require.resolve('./get_schedule_frequency'));
loadTestFile(require.resolve('./create_with_circuit_breaker'));
loadTestFile(require.resolve('./update_with_circuit_breaker'));
loadTestFile(require.resolve('./enable_with_circuit_breaker'));
loadTestFile(require.resolve('./bulk_enable_with_circuit_breaker'));
loadTestFile(require.resolve('./bulk_edit_with_circuit_breaker'));
});
});
}

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../../common/lib';
// eslint-disable-next-line import/no-default-export
export default function updateWithCircuitBreakerTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
describe('Update with circuit breaker', () => {
afterEach(async () => {
await objectRemover.removeAll();
});
it('should prevent rules from being updated if max schedules have been reached', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const updatedData = {
name: 'bcd',
tags: ['bar'],
params: {
foo: true,
},
schedule: { interval: '5s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
};
const { body } = await supertest
.put(`${getUrlPrefix('space1')}/api/alerting/rule/${createdRule2.id}`)
.set('kbn-xsrf', 'foo')
.send(updatedData)
.expect(400);
expect(body.message).eql(
'Error validating update data - Run limit reached: The rule has 12 runs per minute; there are only 7 runs per minute available.'
);
});
it('should allow disabled rules to go over the circuit breaker', async () => {
const { body: createdRule1 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData({ schedule: { interval: '20s' } }))
.expect(200);
objectRemover.add('space1', createdRule1.id, 'rule', 'alerting');
const { body: createdRule2 } = await supertest
.post(`${getUrlPrefix('space1')}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
schedule: { interval: '20s' },
})
)
.expect(200);
objectRemover.add('space1', createdRule2.id, 'rule', 'alerting');
const updatedData = {
name: 'bcd',
tags: ['bar'],
params: {
foo: true,
},
schedule: { interval: '5s' },
actions: [],
throttle: '1m',
notify_when: 'onThrottleInterval',
};
await supertest
.put(`${getUrlPrefix('space1')}/api/alerting/rule/${createdRule2.id}`)
.set('kbn-xsrf', 'foo')
.send(updatedData)
.expect(200);
});
});
}