Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ResponseOps][Rules] Version enable rule route #188645

Merged
merged 9 commits into from
Jul 24, 2024
Merged
12 changes: 12 additions & 0 deletions x-pack/plugins/alerting/common/routes/rule/apis/enable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { enableRuleRequestParamsSchema } from './schemas/latest';
export type { EnableRuleRequestParams } from './types/latest';

export { enableRuleRequestParamsSchema as enableRuleRequestParamsSchemaV1 } from './schemas/v1';
export type { EnableRuleRequestParams as EnableRuleRequestParamsV1 } 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,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { schema } from '@kbn/config-schema';

export const enableRuleRequestParamsSchema = schema.object({
id: 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';
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 type { TypeOf } from '@kbn/config-schema';
import { enableRuleRequestParamsSchemaV1 } from '..';

export type EnableRuleRequestParams = TypeOf<typeof enableRuleRequestParamsSchemaV1>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,43 @@
*/
import { AlertConsumers } from '@kbn/rule-data-utils';

import { RulesClient, ConstructorOptions } from '../rules_client';
import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client';
import {
savedObjectsClientMock,
loggingSystemMock,
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 { TaskStatus } from '@kbn/task-manager-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { migrateLegacyActions } from '../lib';
import { migrateLegacyActionsMock } from '../lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
import { ConnectorAdapterRegistry } from '../../connector_adapters/connector_adapter_registry';
import { API_KEY_PENDING_INVALIDATION_TYPE, RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { backfillClientMock } from '../../backfill_client/backfill_client.mock';

jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => {
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
import { migrateLegacyActions } from '../../../../rules_client/lib';
import { migrateLegacyActionsMock } from '../../../../rules_client/lib/siem_legacy_actions/retrieve_migrated_legacy_actions.mock';
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
import {
API_KEY_PENDING_INVALIDATION_TYPE,
RULE_SAVED_OBJECT_TYPE,
} from '../../../../saved_objects';
import { backfillClientMock } from '../../../../backfill_client/backfill_client.mock';

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 @@ -161,7 +164,7 @@ describe('enable()', () => {

describe('authorization', () => {
test('ensures user is authorised to enable this type of alert under the consumer', async () => {
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });

expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
entity: 'rule',
Expand All @@ -177,7 +180,7 @@ describe('enable()', () => {
new Error(`Unauthorized to enable a "myType" alert for "myApp"`)
);

await expect(rulesClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot(
await expect(rulesClient.enableRule({ id: '1' })).rejects.toMatchInlineSnapshot(
`[Error: Unauthorized to enable a "myType" alert for "myApp"]`
);

Expand All @@ -192,7 +195,7 @@ describe('enable()', () => {

describe('auditLogger', () => {
test('logs audit event when enabling a rule', async () => {
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
Expand All @@ -207,7 +210,7 @@ describe('enable()', () => {
test('logs audit event when not authorised to enable a rule', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));

await expect(rulesClient.enable({ id: '1' })).rejects.toThrow();
await expect(rulesClient.enableRule({ id: '1' })).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
Expand All @@ -230,7 +233,7 @@ describe('enable()', () => {
});

test('enables a rule', async () => {
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -292,7 +295,7 @@ describe('enable()', () => {
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -358,7 +361,7 @@ describe('enable()', () => {
},
});

await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(rulesClientParams.getUserName).not.toHaveBeenCalled();
expect(rulesClientParams.createAPIKey).not.toHaveBeenCalled();
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
Expand All @@ -371,7 +374,7 @@ describe('enable()', () => {
result: { id: '123', name: '123', api_key: 'abc' },
});

await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
'1',
Expand Down Expand Up @@ -423,15 +426,25 @@ describe('enable()', () => {
throw new Error('no');
});
await expect(
async () => await rulesClient.enable({ id: '1' })
async () => await rulesClient.enableRule({ id: '1' })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule - no"`);
expect(taskManager.bulkEnable).not.toHaveBeenCalled();
});

test('throws an error if API params do not match the schema', async () => {
await expect(
// @ts-ignore: this is what we are testing
async () => await rulesClient.enableRule({ id: 1 })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error validating enable rule parameters - [id]: expected value of type [string] but got [number]"`
);
expect(taskManager.bulkEnable).not.toHaveBeenCalled();
});

test('falls back when failing to getDecryptedAsInternalUser', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));

await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith(RULE_SAVED_OBJECT_TYPE, '1');
expect(rulesClientParams.logger.error).toHaveBeenCalledWith(
'enable(): Failed to load API key of alert 1: Fail'
Expand All @@ -443,7 +456,7 @@ describe('enable()', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get'));

await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(rulesClient.enableRule({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to get"`
);
expect(rulesClientParams.getUserName).not.toHaveBeenCalled();
Expand All @@ -460,7 +473,7 @@ describe('enable()', () => {
unsecuredSavedObjectsClient.update.mockReset();
unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update'));

await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(rulesClient.enableRule({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to update"`
);
expect(rulesClientParams.getUserName).toHaveBeenCalled();
Expand All @@ -469,7 +482,7 @@ describe('enable()', () => {
});

test('enables task when scheduledTaskId is defined and task exists', async () => {
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand All @@ -484,7 +497,7 @@ describe('enable()', () => {

test('throws error when enabling task fails', async () => {
taskManager.bulkEnable.mockRejectedValueOnce(new Error('Failed to enable task'));
await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(rulesClient.enableRule({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to enable task"`
);
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
Expand Down Expand Up @@ -513,7 +526,7 @@ describe('enable()', () => {
ownerId: null,
});
taskManager.get.mockRejectedValueOnce(new Error('Failed to get task!'));
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -571,7 +584,7 @@ describe('enable()', () => {
params: {},
ownerId: null,
});
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -626,7 +639,7 @@ describe('enable()', () => {
ownerId: null,
});
taskManager.get.mockResolvedValue({ ...mockTask, status: TaskStatus.Unrecognized });
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -673,7 +686,7 @@ describe('enable()', () => {
attributes: { ...existingRule.attributes, scheduledTaskId: null },
});
taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule'));
await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(rulesClient.enableRule({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to schedule"`
);
expect(rulesClientParams.getUserName).toHaveBeenCalled();
Expand All @@ -690,7 +703,7 @@ describe('enable()', () => {
taskManager.schedule.mockRejectedValueOnce(
Object.assign(new Error('Conflict!'), { statusCode: 409 })
);
await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
Expand Down Expand Up @@ -733,7 +746,7 @@ describe('enable()', () => {
new Error('Fail to update after scheduling task')
);

await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(rulesClient.enableRule({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Fail to update after scheduling task"`
);
expect(rulesClientParams.getUserName).toHaveBeenCalled();
Expand Down Expand Up @@ -766,7 +779,7 @@ describe('enable()', () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedSiemRule);
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);

await rulesClient.enable({ id: '1' });
await rulesClient.enableRule({ id: '1' });

expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,48 @@
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';
import { resetMonitoringLastRun, getNextRun } from '../../lib';
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
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';
import { getRuleCircuitBreakerErrorMessage } from '../../../common';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';

export async function enable(context: RulesClientContext, { id }: { id: string }): Promise<void> {
import { RawRule, IntervalSchedule } from '../../../../types';
import { resetMonitoringLastRun, getNextRun } from '../../../../lib';
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
import { retryIfConflicts } from '../../../../lib/retry_if_conflicts';
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
import { RulesClientContext } from '../../../../rules_client/types';
import {
updateMeta,
createNewAPIKeySet,
scheduleTask,
migrateLegacyActions,
} from '../../../../rules_client/lib';
import { validateScheduleLimit } from '../get_schedule_frequency';
import { getRuleCircuitBreakerErrorMessage } from '../../../../../common';
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
import { EnableRuleParams } from './types';
import { enableRuleParamsSchema } from './schemas';

export async function enableRule(
context: RulesClientContext,
{ id }: EnableRuleParams
): Promise<void> {
return await retryIfConflicts(
context.logger,
`rulesClient.enable('${id}')`,
`rulesClient.enableRule('${id}')`,
async () => await enableWithOCC(context, { id })
);
}

async function enableWithOCC(context: RulesClientContext, { id }: { id: string }) {
async function enableWithOCC(context: RulesClientContext, params: EnableRuleParams) {
let existingApiKey: string | null = null;
let attributes: RawRule;
let version: string | undefined;
let references: SavedObjectReference[];

try {
enableRuleParamsSchema.validate(params);
} catch (error) {
throw Boom.badRequest(`Error validating enable rule parameters - ${error.message}`);
}

const { id } = params;
try {
const decryptedAlert =
await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
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 { EnableRuleParams } from './types';

export { enableRule } from './enable_rule';
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { schema } from '@kbn/config-schema';

export const enableRuleParamsSchema = schema.object({
id: schema.string(),
});
Loading