Skip to content

Commit

Permalink
[RAM][HTTP Versioning] Version Clone Rule Route (#180549)
Browse files Browse the repository at this point in the history
## Summary

Issue: #180079
Parent Issue: #157883

Versions the clone rule API and adds input/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
JiaweiWu and kibanamachine authored Apr 30, 2024
1 parent 5b0a2a5 commit ad81f85
Show file tree
Hide file tree
Showing 20 changed files with 350 additions and 172 deletions.
15 changes: 15 additions & 0 deletions x-pack/plugins/alerting/common/routes/rule/apis/clone/index.ts
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 { cloneRuleRequestParamsSchema } from './schemas/latest';
export type { CloneRuleRequestParams, CloneRuleResponse } from './types/latest';

export { cloneRuleRequestParamsSchema as cloneRuleRequestParamsSchemaV1 } from './schemas/v1';
export type {
CloneRuleRequestParams as CloneRuleRequestParamsV1,
CloneRuleResponse as CloneRuleResponseV1,
} 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 cloneRuleRequestParamsSchema = schema.object({
id: schema.string(),
newId: schema.maybe(schema.string()),
});
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';
16 changes: 16 additions & 0 deletions x-pack/plugins/alerting/common/routes/rule/apis/clone/types/v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

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

export type CloneRuleRequestParams = TypeOf<typeof cloneRuleRequestParamsSchemaV1>;

export interface CloneRuleResponse<Params extends RuleParamsV1 = never> {
body: RuleResponseV1<Params>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import {
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 { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { getBeforeSetup } from './lib';
import { RuleDomain } from '../../application/rule/types';
import { ConstructorOptions, RulesClient } from '../rules_client';
import { backfillClientMock } from '../../backfill_client/backfill_client.mock';
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { getBeforeSetup } from '../../../../rules_client/tests/lib';
import { RuleDomain } from '../../types';
import { ConstructorOptions, RulesClient } from '../../../../rules_client/rules_client';
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';

describe('clone', () => {
const taskManager = taskManagerMock.createStart();
Expand Down Expand Up @@ -129,7 +129,10 @@ describe('clone', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(rule);
unsecuredSavedObjectsClient.create.mockResolvedValue(rule);

const res = await rulesClient.clone('test-rule', { newId: 'test-rule-2' });
const res = await rulesClient.clone({
id: 'test-rule',
newId: 'test-rule-2',
});

expect(res.actions).toEqual([
{
Expand All @@ -151,7 +154,10 @@ describe('clone', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(rule);
unsecuredSavedObjectsClient.create.mockResolvedValue(rule);

await rulesClient.clone('test-rule', { newId: 'test-rule-2' });
await rulesClient.clone({
id: 'test-rule',
newId: 'test-rule-2',
});
const results = unsecuredSavedObjectsClient.create.mock.calls[0][1] as RuleDomain;

expect(results.actions).toMatchInlineSnapshot(`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,50 @@ import Boom from '@hapi/boom';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { SavedObject, SavedObjectsUtils } from '@kbn/core/server';
import { withSpan } from '@kbn/apm-utils';
import { RawRule, SanitizedRule, RuleTypeParams } from '../../types';
import { getDefaultMonitoring } from '../../lib';
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
import { parseDuration } from '../../../common/parse_duration';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { getRuleExecutionStatusPendingAttributes } from '../../lib/rule_execution_status';
import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils';
import { createNewAPIKeySet, createRuleSavedObject } from '../lib';
import { RulesClientContext } from '../types';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { SanitizedRule, RawRule } from '../../../../types';
import { getDefaultMonitoring } from '../../../../lib';
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
import { parseDuration } from '../../../../../common/parse_duration';
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
import { getRuleExecutionStatusPendingAttributes } from '../../../../lib/rule_execution_status';
import { isDetectionEngineAADRuleType } from '../../../../saved_objects/migrations/utils';
import { createNewAPIKeySet, createRuleSavedObject } from '../../../../rules_client/lib';
import { RulesClientContext } from '../../../../rules_client/types';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { CloneRuleParams } from './types';
import { RuleAttributes } from '../../../../data/rule/types';
import { RuleDomain, RuleParams } from '../../types';
import { getDecryptedRuleSo, getRuleSo } from '../../../../data/rule';
import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms';
import { ruleDomainSchema } from '../../schemas';
import { cloneRuleParamsSchema } from './schemas';

export type CloneArguments = [string, { newId?: string }];

export async function clone<Params extends RuleTypeParams = never>(
export async function cloneRule<Params extends RuleParams = never>(
context: RulesClientContext,
id: string,
{ newId }: { newId?: string }
params: CloneRuleParams
): Promise<SanitizedRule<Params>> {
let ruleSavedObject: SavedObject<RawRule>;
const { id, newId } = params;

try {
cloneRuleParamsSchema.validate(params);
} catch (error) {
throw Boom.badRequest(`Error validating clone data - ${error.message}`);
}

let ruleSavedObject: SavedObject<RuleAttributes>;

try {
ruleSavedObject = await withSpan(
{ name: 'encryptedSavedObjectsClient.getDecryptedAsInternalUser', type: 'rules' },
() =>
context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
RULE_SAVED_OBJECT_TYPE,
() => {
return getDecryptedRuleSo({
id,
{
encryptedSavedObjectsClient: context.encryptedSavedObjectsClient,
savedObjectsGetOptions: {
namespace: context.namespace,
}
)
},
});
}
);
} catch (e) {
// We'll skip invalidating the API key since we failed to load the decrypted saved object
Expand All @@ -50,7 +63,12 @@ export async function clone<Params extends RuleTypeParams = never>(
// Still attempt to load the object using SOC
ruleSavedObject = await withSpan(
{ name: 'unsecuredSavedObjectsClient.get', type: 'rules' },
() => context.unsecuredSavedObjectsClient.get<RawRule>(RULE_SAVED_OBJECT_TYPE, id)
() => {
return getRuleSo({
id,
savedObjectsClient: context.unsecuredSavedObjectsClient,
});
}
);
}

Expand All @@ -60,7 +78,8 @@ export async function clone<Params extends RuleTypeParams = never>(
* functionality until we resolve our difference
*/
if (
isDetectionEngineAADRuleType(ruleSavedObject) ||
// TODO (http-versioning): Remove this cast to RawRule
isDetectionEngineAADRuleType(ruleSavedObject as SavedObject<RawRule>) ||
ruleSavedObject.attributes.consumer === AlertConsumers.SIEM
) {
throw Boom.badRequest(
Expand Down Expand Up @@ -107,7 +126,7 @@ export async function clone<Params extends RuleTypeParams = never>(
errorMessage: 'Error creating rule: could not create API key',
});

const rawRule: RawRule = {
const ruleAttributes: RuleAttributes = {
...ruleSavedObject.attributes,
name: ruleName,
...apiKeyAttributes,
Expand All @@ -120,7 +139,10 @@ export async function clone<Params extends RuleTypeParams = never>(
muteAll: false,
mutedInstanceIds: [],
executionStatus: getRuleExecutionStatusPendingAttributes(lastRunTimestamp.toISOString()),
monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
// TODO (http-versioning): Remove this cast to RuleAttributes
monitoring: getDefaultMonitoring(
lastRunTimestamp.toISOString()
) as RuleAttributes['monitoring'],
revision: 0,
scheduledTaskId: null,
running: false,
Expand All @@ -134,12 +156,41 @@ export async function clone<Params extends RuleTypeParams = never>(
})
);

return await withSpan({ name: 'createRuleSavedObject', type: 'rules' }, () =>
createRuleSavedObject(context, {
intervalInMs: parseDuration(rawRule.schedule.interval),
rawRule,
references: ruleSavedObject.references,
ruleId,
})
const clonedRuleAttributes = await withSpan(
{ name: 'createRuleSavedObject', type: 'rules' },
() =>
createRuleSavedObject(context, {
intervalInMs: parseDuration(ruleAttributes.schedule.interval),
rawRule: ruleAttributes,
references: ruleSavedObject.references,
ruleId,
returnRuleAttributes: true,
})
);

// Convert ES RuleAttributes back to domain rule object
const ruleDomain: RuleDomain<Params> = transformRuleAttributesToRuleDomain<Params>(
clonedRuleAttributes.attributes,
{
id: clonedRuleAttributes.id,
logger: context.logger,
ruleType: context.ruleTypeRegistry.get(clonedRuleAttributes.attributes.alertTypeId),
references: clonedRuleAttributes.references,
},
(connectorId: string) => context.isSystemAction(connectorId)
);

// Try to validate created rule, but don't throw.
try {
ruleDomainSchema.validate(ruleDomain);
} catch (e) {
context.logger.warn(`Error validating clone rule domain object for id: ${id}, ${e}`);
}

// Convert domain rule to rule (Remove certain properties)
const rule = transformRuleDomainToRule<Params>(ruleDomain, { isPublic: false });

// TODO (http-versioning): Remove this cast, this enables us to move forward
// without fixing all of other solution types
return rule as SanitizedRule<Params>;
}
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 { CloneRuleParams } from './types';

export { cloneRule } from './clone_rule';
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 cloneRuleParamsSchema = schema.object({
id: schema.string(),
newId: schema.maybe(schema.string()),
});
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 './clone_rule_params_schema';
Original file line number Diff line number Diff line change
@@ -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 { cloneRuleParamsSchema } from '../schemas';

export type CloneRuleParams = TypeOf<typeof cloneRuleParamsSchema>;
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 './clone_rule_params';
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { SavedObjectReference } from '@kbn/core/server';
import { ruleExecutionStatusValues } from '../constants';
import { getRuleSnoozeEndTime } from '../../../lib';
import { RuleDomain, Monitoring, RuleParams } from '../types';
import { PartialRule, SanitizedRule } from '../../../types';
import { RuleAttributes, RuleExecutionStatusAttributes } from '../../../data/rule/types';
import { PartialRule } from '../../../types';
import { UntypedNormalizedRuleType } from '../../../rule_type_registry';
import { injectReferencesIntoParams } from '../../../rules_client/common';
import { getActiveScheduledSnoozes } from '../../../lib/is_rule_snoozed';
Expand Down Expand Up @@ -148,7 +148,7 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
const isSnoozedUntil = includeSnoozeSchedule
? getRuleSnoozeEndTime({
muteAll: esRule.muteAll ?? false,
snoozeSchedule,
snoozeSchedule: snoozeSchedule as SanitizedRule<Params>['snoozeSchedule'],
})?.toISOString()
: null;

Expand Down Expand Up @@ -176,7 +176,7 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
);

const activeSnoozes = getActiveScheduledSnoozes({
snoozeSchedule,
snoozeSchedule: snoozeSchedule as SanitizedRule<Params>['snoozeSchedule'],
muteAll: esRule.muteAll ?? false,
})?.map((s) => s.id);

Expand Down
Loading

0 comments on commit ad81f85

Please sign in to comment.