Skip to content

Commit

Permalink
[RAM][HTTP versioning] Version Bulk Enable Route (elastic#179778)
Browse files Browse the repository at this point in the history
## Summary
Parent Issue: elastic#157883
Issue: elastic#179669

Versions the `PATCH /internal/alerting/rules/_bulk_enable` endpoint with
added input and output validation.

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

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and yuliacech committed May 3, 2024
1 parent 0c2e321 commit cc16d67
Show file tree
Hide file tree
Showing 23 changed files with 604 additions and 141 deletions.
Original file line number Diff line number Diff line change
@@ -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.
*/

export { bulkEnableBodySchema } from './schemas/latest';
export type { BulkEnableRulesRequestBody, BulkEnableRulesResponse } from './types/latest';

export { bulkEnableBodySchema as bulkEnableBodySchemaV1 } from './schemas/v1';
export type {
BulkEnableRulesRequestBody as BulkEnableRulesRequestBodyV1,
BulkEnableRulesResponse as BulkEnableRulesResponseV1,
} from './types/v1';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 bulkEnableBodySchema = schema.object({
filter: schema.maybe(schema.string()),
ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })),
});
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { TypeOf } from '@kbn/config-schema';
import { RuleParamsV1, RuleResponseV1 } from '../../../response';
import { bulkEnableBodySchemaV1 } from '..';

export type BulkEnableRulesRequestBody = TypeOf<typeof bulkEnableBodySchemaV1>;

interface BulkEnableOperationError {
message: string;
status?: number;
rule: {
id: string;
name: string;
};
}

export interface BulkEnableRulesResponse<Params extends RuleParamsV1 = never> {
body: {
rules: Array<RuleResponseV1<Params>>;
errors: BulkEnableOperationError[];
total: number;
task_ids_failed_to_be_enabled: string[];
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
* 2.0.
*/
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client';
import {
savedObjectsClientMock,
savedObjectsRepositoryMock,
uiSettingsServiceMock,
} from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
import { loggerMock } from '@kbn/logging-mocks';
import { BulkUpdateTaskResult } from '@kbn/task-manager-plugin/server/task_scheduling';
import { ActionsClient } from '@kbn/actions-plugin/server';
Expand All @@ -41,24 +41,24 @@ import {
disabledRuleForBulkDisable1,
siemRuleForBulkOps1,
siemRuleForBulkOps2,
} from './test_helpers';
} from '../../../../rules_client/tests/test_helpers';
import { TaskStatus } from '@kbn/task-manager-plugin/server';
import { migrateLegacyActions } from '../lib';
import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { backfillClientMock } from '../../backfill_client/backfill_client.mock';
import { migrateLegacyActions } from '../../../../rules_client/lib';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';

jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
return {
migrateLegacyActions: jest.fn(),
};
});

jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
bulkMarkApiKeysForInvalidation: jest.fn(),
}));

jest.mock('../../application/rule/methods/get_schedule_frequency', () => ({
jest.mock('../get_schedule_frequency', () => ({
validateScheduleLimit: jest.fn(),
}));

Expand Down Expand Up @@ -171,7 +171,7 @@ describe('bulkEnableRules', () => {
rulesClientParams.getActionsClient.mockResolvedValue(actionsClient);
});

test('should enable two rule', async () => {
test('should enable two rules', async () => {
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [enabledRuleForBulkOps1, enabledRuleForBulkOps2],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,43 @@
*/

import pMap from 'p-map';
import Boom from '@hapi/boom';
import { KueryNode, nodeBuilder } from '@kbn/es-query';
import { SavedObjectsBulkUpdateObject, SavedObjectsFindResult } from '@kbn/core/server';
import {
SavedObjectsBulkCreateObject,
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';
import { TaskInstanceWithDeprecatedFields } from '@kbn/task-manager-plugin/server/task';
import { RawRule } from '../../types';
import { convertRuleIdsToKueryNode } from '../../lib';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { bulkCreateRulesSo } from '../../../../data/rule';
import { RawRule, RawRuleAction } from '../../../../types';
import { RuleDomain, RuleParams } from '../../types';
import { convertRuleIdsToKueryNode } from '../../../../lib';
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
import {
retryIfBulkOperationConflicts,
buildKueryNodeFilter,
getAndValidateCommonBulkOptions,
} from '../common';
import { getRuleCircuitBreakerErrorMessage } from '../../../common';
} from '../../../../rules_client/common';
import { getRuleCircuitBreakerErrorMessage, SanitizedRule } from '../../../../../common';
import {
getAuthorizationFilter,
checkAuthorizationAndGetTotal,
updateMeta,
createNewAPIKeySet,
migrateLegacyActions,
} from '../lib';
import { RulesClientContext, BulkOperationError, BulkOptions } from '../types';
import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency';
import { RuleAttributes } from '../../data/rule/types';
import {
transformRuleAttributesToRuleDomain,
transformRuleDomainToRule,
} from '../../application/rule/transforms';
import type { RuleParams } from '../../application/rule/types';
import { ruleDomainSchema } from '../../application/rule/schemas';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
updateMetaAttributes,
} from '../../../../rules_client/lib';
import { RulesClientContext, BulkOperationError } from '../../../../rules_client/types';
import { validateScheduleLimit } from '../get_schedule_frequency';
import { RuleAttributes } from '../../../../data/rule/types';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { BulkEnableRulesParams, BulkEnableRulesResult } from './types';
import { bulkEnableRulesParamsSchema } from './schemas';
import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms';
import { ruleDomainSchema } from '../../schemas';

/**
* Updating too many rules in parallel can cause the denial of service of the
Expand Down Expand Up @@ -71,11 +76,17 @@ const getShouldScheduleTask = async (

export const bulkEnableRules = async <Params extends RuleParams>(
context: RulesClientContext,
options: BulkOptions
) => {
const { ids, filter } = getAndValidateCommonBulkOptions(options);
params: BulkEnableRulesParams
): Promise<BulkEnableRulesResult<Params>> => {
const { ids, filter } = getAndValidateCommonBulkOptions(params);
const actionsClient = await context.getActionsClient();

try {
bulkEnableRulesParamsSchema.validate(params);
} catch (error) {
throw Boom.badRequest(`Error validating bulk enable rules data - ${error.message}`);
}

const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
const authorizationFilter = await getAuthorizationFilter(context, { action: 'ENABLE' });

Expand Down Expand Up @@ -105,12 +116,12 @@ export const bulkEnableRules = async <Params extends RuleParams>(
taskManager: context.taskManager,
});

const enabledRules = rules.map(({ id, attributes, references }) => {
const updatedRules = rules.map(({ id, attributes, references }) => {
// TODO (http-versioning): alertTypeId should never be null, but we need to
// fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject
// when we are doing the bulk disable and this should fix itself
// when we are doing the bulk delete and this should fix itself
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId!);
const ruleDomain = transformRuleAttributesToRuleDomain<Params>(
const ruleDomain: RuleDomain<Params> = transformRuleAttributesToRuleDomain<Params>(
attributes as RuleAttributes,
{
id,
Expand All @@ -125,12 +136,17 @@ export const bulkEnableRules = async <Params extends RuleParams>(
try {
ruleDomainSchema.validate(ruleDomain);
} catch (e) {
context.logger.warn(`Error validating bulk enabled rule domain object for id: ${id}, ${e}`);
context.logger.warn(`Error validating bulk enable rule domain object for id: ${id}, ${e}`);
}
return transformRuleDomainToRule(ruleDomain);
return ruleDomain;
});

return { errors, rules: enabledRules, total, taskIdsFailedToBeEnabled };
// // TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
const updatePublicRules = updatedRules.map((rule: RuleDomain<Params>) => {
return transformRuleDomainToRule<Params>(rule);
}) as Array<SanitizedRule<Params>>;

return { errors, rules: updatePublicRules, total, taskIdsFailedToBeEnabled };
};

const bulkEnableRulesWithOCC = async (
Expand All @@ -145,7 +161,7 @@ const bulkEnableRulesWithOCC = async (
type: 'rules',
},
async () =>
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RawRule>(
await context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<RuleAttributes>(
{
filter: filter ? nodeBuilder.and([filter, additionalFilter]) : additionalFilter,
type: RULE_SAVED_OBJECT_TYPE,
Expand All @@ -155,8 +171,8 @@ const bulkEnableRulesWithOCC = async (
)
);

const rulesFinderRules: Array<SavedObjectsFindResult<RawRule>> = [];
const rulesToEnable: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
const rulesFinderRules: Array<SavedObjectsFindResult<RuleAttributes>> = [];
const rulesToEnable: Array<SavedObjectsBulkUpdateObject<RuleAttributes>> = [];
const tasksToSchedule: TaskInstanceWithDeprecatedFields[] = [];
const errors: BulkOperationError[] = [];
const ruleNameToRuleIdMapping: Record<string, string> = {};
Expand Down Expand Up @@ -207,14 +223,15 @@ const bulkEnableRulesWithOCC = async (
ruleNameToRuleIdMapping[rule.id] = rule.attributes.name;
}

// TODO (http-versioning) Remove RawRuleAction and RawRule casts
const migratedActions = await migrateLegacyActions(context, {
ruleId: rule.id,
actions: rule.attributes.actions,
actions: rule.attributes.actions as RawRuleAction[],
references: rule.references,
attributes: rule.attributes,
attributes: rule.attributes as RawRule,
});

const updatedAttributes = updateMeta(context, {
const updatedAttributes = updateMetaAttributes(context, {
...rule.attributes,
...(!rule.attributes.apiKey &&
(await createNewAPIKeySet(context, {
Expand Down Expand Up @@ -315,12 +332,21 @@ const bulkEnableRulesWithOCC = async (
const result = await withSpan(
{ name: 'unsecuredSavedObjectsClient.bulkCreate', type: 'rules' },
() =>
context.unsecuredSavedObjectsClient.bulkCreate(rulesToEnable, {
overwrite: true,
// TODO (http-versioning): for whatever reasoning we are using SavedObjectsBulkUpdateObject
// everywhere when it should be SavedObjectsBulkCreateObject. We need to fix it in
// bulk_disable, bulk_enable, etc. to fix this cast
bulkCreateRulesSo({
savedObjectsClient: context.unsecuredSavedObjectsClient,
bulkCreateRuleAttributes: rulesToEnable as Array<
SavedObjectsBulkCreateObject<RuleAttributes>
>,
savedObjectsBulkCreateOptions: {
overwrite: true,
},
})
);

const rules: Array<SavedObjectsBulkUpdateObject<RawRule>> = [];
const rules: Array<SavedObjectsBulkUpdateObject<RuleAttributes>> = [];
const taskIdsToEnable: string[] = [];

result.saved_objects.forEach((rule) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { BulkEnableRulesParams, BulkEnableRulesError, BulkEnableRulesResult } from './types';

export { bulkEnableRules } from './bulk_enable_rules';
Original file line number Diff line number Diff line change
@@ -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 bulkEnableRulesParamsSchema = schema.object({
filter: schema.maybe(schema.string()),
ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })),
});
Original file line number Diff line number Diff line change
@@ -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 './bulk_enable_rules_schemas';
Original file line number Diff line number Diff line change
@@ -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 { RuleParams } from '../../../types/rule';
import { SanitizedRule } from '../../../../../types';

export interface BulkEnableRulesParams {
filter?: string;
ids?: string[];
}

export interface BulkEnableRulesError {
message: string;
status?: number;
rule: {
id: string;
name: string;
};
}

// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
export interface BulkEnableRulesResult<Params extends RuleParams> {
rules: Array<SanitizedRule<Params>>;
errors: BulkEnableRulesError[];
total: number;
taskIdsFailedToBeEnabled: string[];
}
Loading

0 comments on commit cc16d67

Please sign in to comment.