Skip to content

Commit

Permalink
[RAM] System action in bulk delete (#170741)
Browse files Browse the repository at this point in the history
Fix: #170392
Meta: #160367


## Summary

This PR enables system actions for the Bulk Delete Rule API.

### 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
  • Loading branch information
guskovaue committed Nov 14, 2023
1 parent 6105ee6 commit d8a7569
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { ActionsClient } from '@kbn/actions-plugin/server';
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
import { RecoveredActionGroup } from '../../../../../common';
Expand All @@ -28,6 +29,10 @@ import {
returnedRuleForBulkOps2,
returnedRuleForBulkOps3,
siemRuleForBulkOps1,
enabledRuleForBulkOpsWithActions1,
enabledRuleForBulkOpsWithActions2,
returnedRuleForBulkEnableWithActions1,
returnedRuleForBulkEnableWithActions2,
} from '../../../../rules_client/tests/test_helpers';
import { migrateLegacyActions } from '../../../../rules_client/lib';
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
Expand Down Expand Up @@ -106,11 +111,12 @@ setGlobalDate();

describe('bulkDelete', () => {
let rulesClient: RulesClient;
let actionsClient: jest.Mocked<ActionsClient>;

const mockCreatePointInTimeFinderAsInternalUser = (
response = {
saved_objects: [enabledRuleForBulkOps1, enabledRuleForBulkOps2, enabledRuleForBulkOps3],
}
} as unknown
) => {
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
.fn()
Expand Down Expand Up @@ -162,6 +168,48 @@ describe('bulkDelete', () => {
},
validLegacyConsumers: [],
});

actionsClient = (await rulesClientParams.getActionsClient()) as jest.Mocked<ActionsClient>;
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action:id');
rulesClientParams.getActionsClient.mockResolvedValue(actionsClient);
});

test('should successfully delete two rule and return right actions', async () => {
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [enabledRuleForBulkOpsWithActions1, enabledRuleForBulkOpsWithActions2],
});
unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({
statuses: [
{ id: 'id1', type: 'alert', success: true },
{ id: 'id2', type: 'alert', success: true },
],
});

const result = await rulesClient.bulkDeleteRules({ filter: 'fake_filter' });

expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledWith(
[enabledRuleForBulkOps1, enabledRuleForBulkOps2].map(({ id }) => ({
id,
type: 'alert',
})),
undefined
);

expect(taskManager.bulkRemove).toHaveBeenCalledTimes(1);
expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1', 'id2']);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
{ apiKeys: ['MTIzOmFiYw==', 'MzIxOmFiYw=='] },
expect.anything(),
expect.anything()
);
expect(result).toStrictEqual({
rules: [returnedRuleForBulkEnableWithActions1, returnedRuleForBulkEnableWithActions2],
errors: [],
total: 2,
taskIdsFailedToBeDeleted: [],
});
});

test('should try to delete rules, two successful and one with 500 error', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const bulkDeleteRules = async <Params extends RuleParams>(
}

const { ids, filter } = options;
const actionsClient = await context.getActionsClient();

const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
const authorizationFilter = await getAuthorizationFilter(context, { action: 'DELETE' });
Expand Down Expand Up @@ -95,13 +96,17 @@ export const bulkDeleteRules = async <Params extends RuleParams>(
// fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject
// when we are doing the bulk delete and this should fix itself
const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId!);
const ruleDomain = transformRuleAttributesToRuleDomain<Params>(attributes as RuleAttributes, {
id,
logger: context.logger,
ruleType,
references,
omitGeneratedValues: false,
});
const ruleDomain = transformRuleAttributesToRuleDomain<Params>(
attributes as RuleAttributes,
{
id,
logger: context.logger,
ruleType,
references,
omitGeneratedValues: false,
},
(connectorId: string) => actionsClient.isSystemAction(connectorId)
);

try {
ruleDomainSchema.validate(ruleDomain);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
*/

import { httpServiceMock } from '@kbn/core/server/mocks';

import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
import { bulkDeleteRulesRoute } from './bulk_delete_rules_route';
import { licenseStateMock } from '../../../../lib/license_state.mock';
import { mockHandlerArguments } from '../../../_mock_handler_arguments';
import { rulesClientMock } from '../../../../rules_client.mock';
import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled';
import { verifyApiAccess } from '../../../../lib/license_api_access';
import {
RuleActionTypes,
RuleDefaultAction,
RuleSystemAction,
SanitizedRule,
} from '../../../../types';

const rulesClient = rulesClientMock.create();

Expand Down Expand Up @@ -123,4 +129,120 @@ describe('bulkDeleteRulesRoute', () => {

expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
});

describe('actions', () => {
const mockedRule: SanitizedRule<{}> = {
id: '1',
alertTypeId: '1',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date(),
updatedAt: new Date(),
actions: [
{
group: 'default',
id: '2',
actionTypeId: 'test',
params: {
foo: true,
},
uuid: '123-456',
type: RuleActionTypes.DEFAULT,
},
],
consumer: 'bar',
name: 'abc',
tags: ['foo'],
enabled: true,
muteAll: false,
notifyWhen: 'onActionGroupChange',
createdBy: '',
updatedBy: '',
apiKeyOwner: '',
throttle: '30s',
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
revision: 0,
};

const action: RuleDefaultAction = {
actionTypeId: 'test',
group: 'default',
id: '2',
params: {
foo: true,
},
uuid: '123-456',
type: RuleActionTypes.DEFAULT,
};

const systemAction: RuleSystemAction = {
actionTypeId: 'test-2',
id: 'system_action-id',
params: {
foo: true,
},
uuid: '123-456',
type: RuleActionTypes.SYSTEM,
};

const mockedRules: Array<SanitizedRule<{}>> = [
{ ...mockedRule, actions: [action, systemAction] },
];

const bulkDeleteActionsResult = {
rules: mockedRules,
errors: [],
total: 1,
taskIdsFailedToBeDeleted: [],
};

it('removes the type from the actions correctly before sending the response', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const actionsClient = actionsClientMock.create();
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id');

bulkDeleteRulesRoute({ router, licenseState });
const [_, handler] = router.patch.mock.calls[0];

rulesClient.bulkDeleteRules.mockResolvedValueOnce(bulkDeleteActionsResult);

const [context, req, res] = mockHandlerArguments(
{ rulesClient, actionsClient },
{
body: bulkDeleteRequest,
},
['ok']
);

const routeRes = await handler(context, req, res);

// @ts-expect-error: body exists
expect(routeRes.body.rules[0].actions).toEqual([
{
connector_type_id: 'test',
group: 'default',
id: '2',
params: {
foo: true,
},
uuid: '123-456',
},
{
connector_type_id: 'test-2',
id: 'system_action-id',
params: {
foo: true,
},
uuid: '123-456',
},
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
RuleTypeParams,
RuleWithLegacyId,
PartialRuleWithLegacyId,
isSystemAction,
} from '../../types';
import {
ruleExecutionStatusFromRaw,
Expand All @@ -23,8 +24,9 @@ import {
} from '../../lib';
import { UntypedNormalizedRuleType } from '../../rule_type_registry';
import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed';
import { injectReferencesIntoActions, injectReferencesIntoParams } from '../common';
import { injectReferencesIntoParams } from '../common';
import { RulesClientContext } from '../types';
import { transformRawActionsToDomainActions } from '../../application/rule/transforms/transform_raw_actions_to_domain_actions';

export interface GetAlertFromRawParams {
id: string;
Expand Down Expand Up @@ -118,14 +120,22 @@ export function getPartialRuleFromRaw<Params extends RuleTypeParams>(
})
: null;
const includeMonitoring = monitoring && !excludeFromPublicApi;

const rule: PartialRule<Params> = {
id,
notifyWhen,
...omit(partialRawRule, excludeFromPublicApi ? [...context.fieldsToExcludeFromPublicApi] : ''),
// we currently only support the Interval Schedule type
// Once we support additional types, this type signature will likely change
schedule: schedule as IntervalSchedule,
actions: actions ? injectReferencesIntoActions(id, actions, references || []) : [],
actions: actions
? transformRawActionsToDomainActions({
ruleId: id,
actions,
references: references || [],
isSystemAction: context.isSystemAction,
})
: [],
params: injectReferencesIntoParams(id, ruleType, params, references || []) as Params,
...(excludeFromPublicApi ? {} : { snoozeSchedule: snoozeScheduleDates ?? [] }),
...(includeSnoozeData && !excludeFromPublicApi
Expand Down Expand Up @@ -161,7 +171,12 @@ export function getPartialRuleFromRaw<Params extends RuleTypeParams>(

if (omitGeneratedValues) {
if (rule.actions) {
rule.actions = rule.actions.map((ruleAction) => omit(ruleAction, 'alertsFilter.query.dsl'));
rule.actions = rule.actions.map((ruleAction) => {
if (!isSystemAction(ruleAction)) {
return omit(ruleAction, 'alertsFilter.query.dsl');
}
return ruleAction;
});
}
}

Expand Down

0 comments on commit d8a7569

Please sign in to comment.