mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Actions] System actions MVP (#166267)
## Summary A system action is an action that triggers Kibana workflows—for example, creating a case, running an OsQuery, running an ML job, or logging. In this PR: - Enable rule routes to accept system actions. The schema of the action is not changed. The framework deducts which action is a system action automatically. System actions do not accept properties like the `notifyWhen` or `group`. - Enable rule client methods to accept system actions. The methods accept a new property called `systemActions`. The methods merge the actions with the system actions before persisting the rule to ES. The methods split the actions from the system actions and return two arrays, `actions` and `systemActions`. - Introduce connector adapters: a way to transform the action params to the corresponding connector params. - Allow the execution of system actions. Only alert summaries are supported. Users cannot control the execution of system actions. - Register an example system action. - Change the UI to handle system action. All configuration regarding execution like "Run when" is hidden for system actions. Users cannot select the same system action twice. Closes https://github.com/elastic/kibana/issues/160367 This PR merges the system actions framework, a culmination of several issues merged to the `system_actions_mvp` feature branch over the past several months. ## Testing A system action with ID `system-connector-.system-log-example` will be available to be used by the APIs and the UI if you start Kibana with `--run-examples`. Please ensure the following: - You can create and update rules with actions and system actions. - A rule with actions and system actions is executed as expected. - Entries about the system action execution are added to the event log as expected. - Existing rules with actions work without issues (BWC). - You can perform bulk actions in the rules table to rules with actions and system actions. - License restrictions are respected. - Permission restrictions are respected. - Disabled system actions cannot be used. - Users cannot specify how the system action will run in the UI and the API. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Julia <iuliia.guskova@elastic.co> Co-authored-by: Zacqary Xeper <zacqary.xeper@elastic.co> Co-authored-by: Zacqary Adam Xeper <Zacqary@users.noreply.github.com> Co-authored-by: Ying Mao <ying.mao@elastic.co> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
0dfa0d0eb4
commit
26d82227d2
264 changed files with 13676 additions and 2006 deletions
|
@ -4,7 +4,7 @@
|
|||
"owner": "@elastic/response-ops",
|
||||
"plugin": {
|
||||
"id": "triggersActionsUiExample",
|
||||
"server": false,
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"triggersActionsUi",
|
||||
|
@ -12,10 +12,9 @@
|
|||
"alerting",
|
||||
"developerExamples",
|
||||
"kibanaReact",
|
||||
"cases"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"spaces"
|
||||
"cases",
|
||||
"actions"
|
||||
],
|
||||
"optionalPlugins": ["spaces"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type {
|
||||
ActionTypeModel as ConnectorTypeModel,
|
||||
GenericValidationResult,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { SystemLogActionParams } from '../types';
|
||||
|
||||
export function getConnectorType(): ConnectorTypeModel<unknown, unknown, SystemLogActionParams> {
|
||||
return {
|
||||
id: '.system-log-example',
|
||||
iconClass: 'logsApp',
|
||||
selectMessage: i18n.translate(
|
||||
'xpack.stackConnectors.components.systemLogExample.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Example of a system action that sends logs to the Kibana server',
|
||||
}
|
||||
),
|
||||
actionTypeTitle: i18n.translate(
|
||||
'xpack.stackConnectors.components.serverLog.connectorTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Send to System log - Example',
|
||||
}
|
||||
),
|
||||
validateParams: (
|
||||
actionParams: SystemLogActionParams
|
||||
): Promise<GenericValidationResult<Pick<SystemLogActionParams, 'message'>>> => {
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
const validationResult = { errors };
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.stackConnectors.components.serverLog.error.requiredServerLogMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return Promise.resolve(validationResult);
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: lazy(() => import('./system_log_example_params')),
|
||||
isSystemActionType: true,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 React, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { TextAreaWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SystemLogActionParams } from '../types';
|
||||
|
||||
export const ServerLogParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<SystemLogActionParams>
|
||||
> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
messageVariables,
|
||||
defaultMessage,
|
||||
useDefaultMessage,
|
||||
}) => {
|
||||
const { message } = actionParams;
|
||||
|
||||
const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState<
|
||||
[boolean, string | undefined]
|
||||
>([false, defaultMessage]);
|
||||
// This params component is derived primarily from server_log_params.tsx, see that file and its
|
||||
// corresponding unit tests for details on functionality
|
||||
useEffect(() => {
|
||||
if (
|
||||
useDefaultMessage ||
|
||||
!actionParams?.message ||
|
||||
(isUsingDefault &&
|
||||
actionParams?.message === defaultMessageUsed &&
|
||||
defaultMessageUsed !== defaultMessage)
|
||||
) {
|
||||
setDefaultMessageUsage([true, defaultMessage]);
|
||||
editAction('message', defaultMessage, index);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultMessage]);
|
||||
|
||||
return (
|
||||
<TextAreaWithMessageVariables
|
||||
index={index}
|
||||
editAction={editAction}
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'message'}
|
||||
inputTargetValue={message}
|
||||
label={i18n.translate(
|
||||
'xpack.stackConnectors.components.systemLogExample.logMessageFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
)}
|
||||
errors={errors.message as string[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ServerLogParamsFields as default };
|
|
@ -5,9 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const filterStateStore = {
|
||||
APP_STATE: 'appState',
|
||||
GLOBAL_STATE: 'globalState',
|
||||
} as const;
|
||||
|
||||
export type FilterStateStore = typeof filterStateStore[keyof typeof filterStateStore];
|
||||
export interface SystemLogActionParams {
|
||||
message: string;
|
||||
}
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { SortCombinations } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { getConnectorType as getSystemLogExampleConnectorType } from './connector_types/system_log_example/system_log_example';
|
||||
|
||||
export interface TriggersActionsUiExamplePublicSetupDeps {
|
||||
alerting: AlertingSetup;
|
||||
|
@ -145,6 +146,8 @@ export class TriggersActionsUiExamplePlugin
|
|||
};
|
||||
|
||||
alertsTableConfigurationRegistry.register(config);
|
||||
|
||||
triggersActionsUi.actionTypeRegistry.register(getSystemLogExampleConnectorType());
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import { LogMeta } from '@kbn/core/server';
|
||||
import type {
|
||||
ActionType as ConnectorType,
|
||||
ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
|
||||
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
|
||||
} from '@kbn/actions-plugin/server/types';
|
||||
import {
|
||||
AlertingConnectorFeatureId,
|
||||
UptimeConnectorFeatureId,
|
||||
} from '@kbn/actions-plugin/common/connector_feature_config';
|
||||
import { ConnectorAdapter } from '@kbn/alerting-plugin/server';
|
||||
|
||||
// see: https://en.wikipedia.org/wiki/Unicode_control_characters
|
||||
// but don't include tabs (0x09), they're fine
|
||||
const CONTROL_CHAR_PATTERN = /[\x00-\x08]|[\x0A-\x1F]|[\x7F-\x9F]|[\u2028-\u2029]/g;
|
||||
|
||||
// replaces control characters in string with ;, but leaves tabs
|
||||
function withoutControlCharacters(s: string): string {
|
||||
return s.replace(CONTROL_CHAR_PATTERN, ';');
|
||||
}
|
||||
|
||||
export type ServerLogConnectorType = ConnectorType<{}, {}, ActionParamsType>;
|
||||
export type ServerLogConnectorTypeExecutorOptions = ConnectorTypeExecutorOptions<
|
||||
{},
|
||||
{},
|
||||
ActionParamsType
|
||||
>;
|
||||
|
||||
// params definition
|
||||
|
||||
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
|
||||
|
||||
const ParamsSchema = schema.object({
|
||||
message: schema.string(),
|
||||
});
|
||||
|
||||
export const ConnectorTypeId = '.system-log-example';
|
||||
// connector type definition
|
||||
export function getConnectorType(): ServerLogConnectorType {
|
||||
return {
|
||||
id: ConnectorTypeId,
|
||||
isSystemActionType: true,
|
||||
minimumLicenseRequired: 'gold', // Third party action types require at least gold
|
||||
name: i18n.translate('xpack.stackConnectors.systemLogExample.title', {
|
||||
defaultMessage: 'System log - example',
|
||||
}),
|
||||
supportedFeatureIds: [AlertingConnectorFeatureId, UptimeConnectorFeatureId],
|
||||
validate: {
|
||||
config: { schema: schema.object({}, { defaultValue: {} }) },
|
||||
secrets: { schema: schema.object({}, { defaultValue: {} }) },
|
||||
params: {
|
||||
schema: ParamsSchema,
|
||||
},
|
||||
},
|
||||
executor,
|
||||
};
|
||||
}
|
||||
|
||||
export const connectorAdapter: ConnectorAdapter = {
|
||||
connectorTypeId: ConnectorTypeId,
|
||||
ruleActionParamsSchema: ParamsSchema,
|
||||
buildActionParams: ({ alerts, rule, params, spaceId, ruleUrl }) => {
|
||||
return { ...params };
|
||||
},
|
||||
};
|
||||
|
||||
// action executor
|
||||
|
||||
async function executor(
|
||||
execOptions: ServerLogConnectorTypeExecutorOptions
|
||||
): Promise<ConnectorTypeExecutorResult<void>> {
|
||||
const { actionId, params, logger } = execOptions;
|
||||
const sanitizedMessage = withoutControlCharacters(params.message);
|
||||
try {
|
||||
logger.info<LogMeta>(`SYSTEM ACTION EXAMPLE Server log: ${sanitizedMessage}`);
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.stackConnectors.serverLog.errorLoggingErrorMessage', {
|
||||
defaultMessage: 'error logging message',
|
||||
});
|
||||
return {
|
||||
status: 'error',
|
||||
message,
|
||||
serviceMessage: err.message,
|
||||
actionId,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'ok', actionId };
|
||||
}
|
13
x-pack/examples/triggers_actions_ui_example/server/index.ts
Normal file
13
x-pack/examples/triggers_actions_ui_example/server/index.ts
Normal 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 { PluginInitializer } from '@kbn/core/server';
|
||||
|
||||
export const plugin: PluginInitializer<void, void> = async () => {
|
||||
const { TriggersActionsUiExamplePlugin } = await import('./plugin');
|
||||
return new TriggersActionsUiExamplePlugin();
|
||||
};
|
33
x-pack/examples/triggers_actions_ui_example/server/plugin.ts
Normal file
33
x-pack/examples/triggers_actions_ui_example/server/plugin.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
|
||||
import { PluginSetupContract as ActionsSetup } from '@kbn/actions-plugin/server';
|
||||
import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/server';
|
||||
|
||||
import {
|
||||
getConnectorType as getSystemLogExampleConnectorType,
|
||||
connectorAdapter as systemLogConnectorAdapter,
|
||||
} from './connector_types/system_log_example';
|
||||
|
||||
// this plugin's dependencies
|
||||
export interface TriggersActionsUiExampleDeps {
|
||||
alerting: AlertingSetup;
|
||||
actions: ActionsSetup;
|
||||
}
|
||||
export class TriggersActionsUiExamplePlugin
|
||||
implements Plugin<void, void, TriggersActionsUiExampleDeps>
|
||||
{
|
||||
public setup(core: CoreSetup, { actions, alerting }: TriggersActionsUiExampleDeps) {
|
||||
actions.registerType(getSystemLogExampleConnectorType());
|
||||
alerting.registerConnectorAdapter(systemLogConnectorAdapter);
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
|
@ -23,5 +23,8 @@
|
|||
"@kbn/data-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/i18n",
|
||||
"@kbn/actions-plugin",
|
||||
"@kbn/config-schema",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -495,7 +495,7 @@ describe('actionTypeRegistry', () => {
|
|||
expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true);
|
||||
});
|
||||
|
||||
test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has system connectors', async () => {
|
||||
test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has system connectors', async () => {
|
||||
mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false);
|
||||
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
|
||||
|
||||
|
@ -504,7 +504,7 @@ describe('actionTypeRegistry', () => {
|
|||
'system-connector-test.system-action',
|
||||
'system-action-type'
|
||||
)
|
||||
).toEqual(false);
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => {
|
||||
|
|
|
@ -92,7 +92,11 @@ export class ActionTypeRegistry {
|
|||
(connector) => connector.id === actionId
|
||||
);
|
||||
|
||||
return actionTypeEnabled || (!actionTypeEnabled && inMemoryConnector?.isPreconfigured === true);
|
||||
return (
|
||||
actionTypeEnabled ||
|
||||
(!actionTypeEnabled &&
|
||||
(inMemoryConnector?.isPreconfigured === true || inMemoryConnector?.isSystemAction === true))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,7 @@ const createActionsClientMock = () => {
|
|||
delete: jest.fn(),
|
||||
update: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
getAllSystemConnectors: jest.fn(),
|
||||
getBulk: jest.fn(),
|
||||
getOAuthAccessToken: jest.fn(),
|
||||
execute: jest.fn(),
|
||||
|
|
|
@ -2797,7 +2797,7 @@ describe('execute()', () => {
|
|||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
actionTypeId: '.cases',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
@ -2930,7 +2930,7 @@ describe('execute()', () => {
|
|||
});
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
actionTypeId: 'my-action-type',
|
||||
actionTypeId: '.cases',
|
||||
operation: 'execute',
|
||||
additionalPrivileges: ['test/create'],
|
||||
});
|
||||
|
|
|
@ -91,6 +91,7 @@ import {
|
|||
} from '../lib/get_execution_log_aggregation';
|
||||
import { connectorFromSavedObject, isConnectorDeprecated } from '../application/connector/lib';
|
||||
import { ListTypesParams } from '../application/connector/methods/list_types/types';
|
||||
import { getAllSystemConnectors } from '../application/connector/methods/get_all/get_all';
|
||||
|
||||
interface ActionUpdate {
|
||||
name: string;
|
||||
|
@ -418,6 +419,13 @@ export class ActionsClient {
|
|||
return getAll({ context: this.context, includeSystemActions });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all system connectors
|
||||
*/
|
||||
public async getAllSystemConnectors(): Promise<ConnectorWithExtraFindData[]> {
|
||||
return getAllSystemConnectors({ context: this.context });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bulk actions with in-memory list
|
||||
*/
|
||||
|
@ -691,7 +699,7 @@ export class ActionsClient {
|
|||
let actionTypeId: string | undefined;
|
||||
|
||||
try {
|
||||
if (this.isPreconfigured(actionId)) {
|
||||
if (this.isPreconfigured(actionId) || this.isSystemAction(actionId)) {
|
||||
const connector = this.context.inMemoryConnectors.find(
|
||||
(inMemoryConnector) => inMemoryConnector.id === actionId
|
||||
);
|
||||
|
|
|
@ -113,8 +113,152 @@ describe('getAll()', () => {
|
|||
getEventLogClient.mockResolvedValue(eventLogClient);
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
function getAllOperation(): ReturnType<ActionsClient['getAll']> {
|
||||
describe('getAll()', () => {
|
||||
describe('authorization', () => {
|
||||
function getAllOperation(): ReturnType<ActionsClient['getAll']> {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
return actionsClient.getAll();
|
||||
}
|
||||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getAllOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get all actions`)
|
||||
);
|
||||
|
||||
await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
test('logs audit event when searching connectors', async () => {
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await actionsClient.getAll();
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'connector_find',
|
||||
outcome: 'success',
|
||||
}),
|
||||
kibana: { saved_object: { id: '1', type: 'action' } },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('logs audit event when not authorised to search connectors', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
|
||||
|
||||
await expect(actionsClient.getAll()).rejects.toThrow();
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'connector_find',
|
||||
outcome: 'failure',
|
||||
}),
|
||||
error: { code: 'Error', message: 'Unauthorized' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('calls unsecuredSavedObjectsClient with parameters and returns inMemoryConnectors correctly', async () => {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
|
@ -125,6 +269,7 @@ describe('getAll()', () => {
|
|||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
|
@ -135,6 +280,215 @@ describe('getAll()', () => {
|
|||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
'system-connector-.cases': { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
/**
|
||||
* System actions will not
|
||||
* be returned from getAll
|
||||
* if no options are provided
|
||||
*/
|
||||
{
|
||||
id: 'system-connector-.cases',
|
||||
actionTypeId: '.cases',
|
||||
name: 'System action: .cases',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const result = await actionsClient.getAll();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: { foo: 'bar' },
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
referencedByCount: 6,
|
||||
},
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
name: 'test',
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('get system actions correctly', async () => {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
'system-connector-.cases': { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'system-connector-.cases',
|
||||
actionTypeId: '.cases',
|
||||
name: 'System action: .cases',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const result = await actionsClient.getAll({ includeSystemActions: true });
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
actionTypeId: '.cases',
|
||||
id: 'system-connector-.cases',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
name: 'System action: .cases',
|
||||
referencedByCount: 2,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: { foo: 'bar' },
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
referencedByCount: 6,
|
||||
},
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
name: 'test',
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('validates connectors before return', async () => {
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
|
@ -173,387 +527,183 @@ describe('getAll()', () => {
|
|||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
return actionsClient.getAll();
|
||||
}
|
||||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getAllOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
const result = await actionsClient.getAll({ includeSystemActions: true });
|
||||
expect(result).toEqual([
|
||||
{
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
id: '1',
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
referencedByCount: 6,
|
||||
},
|
||||
{
|
||||
actionTypeId: '.slack',
|
||||
id: 'testPreconfigured',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
referencedByCount: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
test('throws when user is not authorised to create the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get all actions`)
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'Error validating connector: 1, Error: [actionTypeId]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
test('logs audit event when searching connectors', async () => {
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
describe('getAllSystemConnectors()', () => {
|
||||
describe('authorization', () => {
|
||||
function getAllOperation(): ReturnType<ActionsClient['getAll']> {
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
aggregations: {
|
||||
'system-connector-.test': { doc_count: 2 },
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'system-connector-.test',
|
||||
actionTypeId: '.test',
|
||||
name: 'Test system action',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
return actionsClient.getAllSystemConnectors();
|
||||
}
|
||||
|
||||
test('ensures user is authorised to get the type of action', async () => {
|
||||
await getAllOperation();
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to get the type of action', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get all actions`)
|
||||
);
|
||||
|
||||
await expect(getAllOperation()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get all actions]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ operation: 'get' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
test('logs audit event when not authorised to search connectors', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
|
||||
|
||||
await expect(actionsClient.getAllSystemConnectors()).rejects.toThrow();
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'connector_find',
|
||||
outcome: 'failure',
|
||||
}),
|
||||
error: { code: 'Error', message: 'Unauthorized' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('get all system actions correctly', async () => {
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
'system-connector-.test': { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await actionsClient.getAll();
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'connector_find',
|
||||
outcome: 'success',
|
||||
}),
|
||||
kibana: { saved_object: { id: '1', type: 'action' } },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('logs audit event when not authorised to search connectors', async () => {
|
||||
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
|
||||
|
||||
await expect(actionsClient.getAll()).rejects.toThrow();
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'connector_find',
|
||||
outcome: 'failure',
|
||||
}),
|
||||
error: { code: 'Error', message: 'Unauthorized' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('calls unsecuredSavedObjectsClient with parameters and returns inMemoryConnectors correctly', async () => {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: 'my-action-type',
|
||||
secrets: {
|
||||
test: 'test1',
|
||||
},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
'system-connector-.cases': { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
{
|
||||
id: 'system-connector-.test',
|
||||
actionTypeId: '.test',
|
||||
name: 'Test system action',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* System actions will not
|
||||
* be returned from getAll
|
||||
* if no options are provided
|
||||
*/
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const result = await actionsClient.getAllSystemConnectors();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'system-connector-.cases',
|
||||
actionTypeId: '.cases',
|
||||
name: 'System action: .cases',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
id: 'system-connector-.test',
|
||||
actionTypeId: '.test',
|
||||
name: 'Test system action',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
referencedByCount: 2,
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
]);
|
||||
});
|
||||
|
||||
const result = await actionsClient.getAll();
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: { foo: 'bar' },
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
referencedByCount: 6,
|
||||
},
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
name: 'test',
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('get system actions correctly', async () => {
|
||||
const expectedResult = {
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult);
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
'system-connector-.cases': { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'system-connector-.cases',
|
||||
actionTypeId: '.cases',
|
||||
name: 'System action: .cases',
|
||||
config: {},
|
||||
secrets: {},
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const result = await actionsClient.getAll({ includeSystemActions: true });
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
actionTypeId: '.cases',
|
||||
id: 'system-connector-.cases',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
name: 'System action: .cases',
|
||||
referencedByCount: 2,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: { foo: 'bar' },
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
referencedByCount: 6,
|
||||
},
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
name: 'test',
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('validates connectors before return', async () => {
|
||||
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'test',
|
||||
isMissingSecrets: false,
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
scopedClusterClient.asInternalUser.search.mockResponse(
|
||||
// @ts-expect-error not full search response
|
||||
{
|
||||
aggregations: {
|
||||
'1': { doc_count: 6 },
|
||||
testPreconfigured: { doc_count: 2 },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
actionsClient = new ActionsClient({
|
||||
logger,
|
||||
actionTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
scopedClusterClient,
|
||||
kibanaIndices,
|
||||
actionExecutor,
|
||||
ephemeralExecutionEnqueuer,
|
||||
bulkExecutionEnqueuer,
|
||||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
inMemoryConnectors: [
|
||||
{
|
||||
id: 'testPreconfigured',
|
||||
actionTypeId: '.slack',
|
||||
secrets: {},
|
||||
isPreconfigured: true,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const result = await actionsClient.getAll({ includeSystemActions: true });
|
||||
expect(result).toEqual([
|
||||
{
|
||||
config: {
|
||||
foo: 'bar',
|
||||
},
|
||||
id: '1',
|
||||
isDeprecated: false,
|
||||
isMissingSecrets: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
referencedByCount: 6,
|
||||
},
|
||||
{
|
||||
actionTypeId: '.slack',
|
||||
id: 'testPreconfigured',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: true,
|
||||
isSystemAction: false,
|
||||
name: 'test',
|
||||
referencedByCount: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'Error validating connector: 1, Error: [actionTypeId]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -127,6 +127,12 @@ async function getAllHelper({
|
|||
connectors: mergedResult,
|
||||
});
|
||||
|
||||
validateConnectors(connectors, logger);
|
||||
|
||||
return connectors;
|
||||
}
|
||||
|
||||
const validateConnectors = (connectors: ConnectorWithExtraFindData[], logger: Logger) => {
|
||||
connectors.forEach((connector) => {
|
||||
// Try to validate the connectors, but don't throw.
|
||||
try {
|
||||
|
@ -135,6 +141,48 @@ async function getAllHelper({
|
|||
logger.warn(`Error validating connector: ${connector.id}, ${e}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export async function getAllSystemConnectors({
|
||||
context,
|
||||
}: {
|
||||
context: GetAllParams['context'];
|
||||
}): Promise<ConnectorWithExtraFindData[]> {
|
||||
try {
|
||||
await context.authorization.ensureAuthorized({ operation: 'get' });
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
action: ConnectorAuditAction.FIND,
|
||||
error,
|
||||
})
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const systemConnectors = context.inMemoryConnectors.filter(
|
||||
(connector) => connector.isSystemAction
|
||||
);
|
||||
|
||||
const transformedSystemConnectors = systemConnectors
|
||||
.map((systemConnector) => ({
|
||||
id: systemConnector.id,
|
||||
actionTypeId: systemConnector.actionTypeId,
|
||||
name: systemConnector.name,
|
||||
isPreconfigured: systemConnector.isPreconfigured,
|
||||
isDeprecated: isConnectorDeprecated(systemConnector),
|
||||
isSystemAction: systemConnector.isSystemAction,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const connectors = await injectExtraFindData({
|
||||
kibanaIndices: context.kibanaIndices,
|
||||
esClient: context.scopedClusterClient.asInternalUser,
|
||||
connectors: transformedSystemConnectors,
|
||||
});
|
||||
|
||||
validateConnectors(connectors, context.logger);
|
||||
|
||||
return connectors;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '@kbn/core/server/mocks';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { actionsClientMock } from './actions_client/actions_client.mock';
|
||||
import { actionsClientMock, ActionsClientMock } from './actions_client/actions_client.mock';
|
||||
import { PluginSetupContract, PluginStartContract, renderActionParameterTemplates } from './plugin';
|
||||
import { Services, UnsecuredServices } from './types';
|
||||
import { actionsAuthorizationMock } from './authorization/actions_authorization.mock';
|
||||
|
@ -21,6 +21,8 @@ import { ConnectorTokenClient } from './lib/connector_token_client';
|
|||
import { unsecuredActionsClientMock } from './unsecured_actions_client/unsecured_actions_client.mock';
|
||||
export { actionsAuthorizationMock };
|
||||
export { actionsClientMock };
|
||||
export type { ActionsClientMock };
|
||||
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
const createSetupMock = () => {
|
||||
|
@ -49,6 +51,7 @@ const createStartMock = () => {
|
|||
.mockReturnValue(actionsAuthorizationMock.create()),
|
||||
inMemoryConnectors: [],
|
||||
renderActionParameterTemplates: jest.fn(),
|
||||
isSystemActionConnector: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -896,5 +896,53 @@ describe('Actions Plugin', () => {
|
|||
expect(pluginSetup.getActionsHealth()).toEqual({ hasPermanentEncryptionKey: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSystemActionConnector()', () => {
|
||||
it('should return true if the connector is a system connector', async () => {
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
|
||||
pluginSetup.registerType({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
expect(pluginStart.isSystemActionConnector('system-connector-.cases')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the connector is not a system connector', async () => {
|
||||
// coreMock.createSetup doesn't support Plugin generics
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
|
||||
|
||||
pluginSetup.registerType({
|
||||
id: '.cases',
|
||||
name: 'Cases',
|
||||
minimumLicenseRequired: 'platinum',
|
||||
supportedFeatureIds: ['alerting'],
|
||||
validate: {
|
||||
config: { schema: schema.object({}) },
|
||||
secrets: { schema: schema.object({}) },
|
||||
params: { schema: schema.object({}) },
|
||||
},
|
||||
isSystemActionType: true,
|
||||
executor,
|
||||
});
|
||||
|
||||
const pluginStart = await plugin.start(coreStart, pluginsStart);
|
||||
expect(pluginStart.isSystemActionConnector('preconfiguredServerLog')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -159,6 +159,7 @@ export interface PluginStartContract {
|
|||
params: Params,
|
||||
variables: Record<string, unknown>
|
||||
): Params;
|
||||
isSystemActionConnector: (connectorId: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ActionsPluginsSetup {
|
||||
|
@ -603,6 +604,12 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
inMemoryConnectors: this.inMemoryConnectors,
|
||||
renderActionParameterTemplates: (...args) =>
|
||||
renderActionParameterTemplates(this.logger, actionTypeRegistry, ...args),
|
||||
isSystemActionConnector: (connectorId: string): boolean => {
|
||||
return this.inMemoryConnectors.some(
|
||||
(inMemoryConnector) =>
|
||||
inMemoryConnector.isSystemAction && inMemoryConnector.id === connectorId
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { getAllConnectorsIncludingSystemRoute } from './get_all_system';
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../../../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
|
||||
import { verifyAccessAndContext } from '../../verify_access_and_context';
|
||||
import { actionsClientMock } from '../../../actions_client/actions_client.mock';
|
||||
|
||||
jest.mock('../../verify_access_and_context', () => ({
|
||||
verifyAccessAndContext: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
|
||||
});
|
||||
|
||||
describe('getAllConnectorsIncludingSystemRoute', () => {
|
||||
it('get all connectors with proper parameters', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getAllConnectorsIncludingSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connectors"`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.getAll.mockResolvedValueOnce([
|
||||
{
|
||||
id: '.system-action-id',
|
||||
isPreconfigured: false,
|
||||
isSystemAction: true,
|
||||
isDeprecated: false,
|
||||
name: 'my system action',
|
||||
actionTypeId: '.system-action-type',
|
||||
isMissingSecrets: false,
|
||||
config: {},
|
||||
referencedByCount: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"config": Object {},
|
||||
"connector_type_id": ".system-action-type",
|
||||
"id": ".system-action-id",
|
||||
"is_deprecated": false,
|
||||
"is_missing_secrets": false,
|
||||
"is_preconfigured": false,
|
||||
"is_system_action": true,
|
||||
"name": "my system action",
|
||||
"referenced_by_count": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(actionsClient.getAll).toHaveBeenCalledWith({ includeSystemActions: true });
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: [
|
||||
{
|
||||
config: {},
|
||||
connector_type_id: '.system-action-type',
|
||||
id: '.system-action-id',
|
||||
is_deprecated: false,
|
||||
is_missing_secrets: false,
|
||||
is_preconfigured: false,
|
||||
is_system_action: true,
|
||||
name: 'my system action',
|
||||
referenced_by_count: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures the license allows getting all connectors', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getAllConnectorsIncludingSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connectors"`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.getAll.mockResolvedValueOnce([]);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
|
||||
});
|
||||
|
||||
it('ensures the license check prevents getting all connectors', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
(verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => {
|
||||
throw new Error('OMG');
|
||||
});
|
||||
|
||||
getAllConnectorsIncludingSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connectors"`);
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.getAll.mockResolvedValueOnce([]);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
|
||||
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
|
||||
|
||||
expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { AllConnectorsResponseV1 } from '../../../../common/routes/connector/response';
|
||||
import { ActionsRequestHandlerContext } from '../../../types';
|
||||
import { INTERNAL_BASE_ACTION_API_PATH } from '../../../../common';
|
||||
import { ILicenseState } from '../../../lib';
|
||||
import { verifyAccessAndContext } from '../../verify_access_and_context';
|
||||
import { transformGetAllConnectorsResponseV1 } from '../get_all/transforms';
|
||||
|
||||
export const getAllConnectorsIncludingSystemRoute = (
|
||||
router: IRouter<ActionsRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ACTION_API_PATH}/connectors`,
|
||||
validate: {},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const actionsClient = (await context.actions).getActionsClient();
|
||||
const result = await actionsClient.getAll({
|
||||
includeSystemActions: true,
|
||||
});
|
||||
|
||||
const responseBody: AllConnectorsResponseV1[] = transformGetAllConnectorsResponseV1(result);
|
||||
return res.ok({ body: responseBody });
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -5,9 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const filterStateStore = {
|
||||
APP_STATE: 'appState',
|
||||
GLOBAL_STATE: 'globalState',
|
||||
} as const;
|
||||
|
||||
export type FilterStateStore = typeof filterStateStore[keyof typeof filterStateStore];
|
||||
export { getAllConnectorsIncludingSystemRoute } from './get_all_system';
|
|
@ -35,7 +35,9 @@ export const listTypesRoute = (
|
|||
// Assert versioned inputs
|
||||
const query: ConnectorTypesRequestQueryV1 = req.query;
|
||||
|
||||
const connectorTypes = await actionsClient.listTypes({ featureId: query?.feature_id });
|
||||
const connectorTypes = await actionsClient.listTypes({
|
||||
featureId: query?.feature_id,
|
||||
});
|
||||
|
||||
const responseBody: ConnectorTypesResponseV1[] =
|
||||
transformListTypesResponseV1(connectorTypes);
|
||||
|
|
|
@ -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 { listTypesWithSystemRoute } from './list_types_system';
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { LicenseType } from '@kbn/licensing-plugin/server';
|
||||
import { licenseStateMock } from '../../../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from '../../legacy/_mock_handler_arguments';
|
||||
import { listTypesWithSystemRoute } from './list_types_system';
|
||||
import { verifyAccessAndContext } from '../../verify_access_and_context';
|
||||
import { actionsClientMock } from '../../../mocks';
|
||||
|
||||
jest.mock('../../verify_access_and_context', () => ({
|
||||
verifyAccessAndContext: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
|
||||
});
|
||||
|
||||
describe('listTypesWithSystemRoute', () => {
|
||||
it('lists action types with proper parameters', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
listTypesWithSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector_types"`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'gold' as LicenseType,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
isSystemActionType: true,
|
||||
},
|
||||
];
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"enabled_in_config": true,
|
||||
"enabled_in_license": true,
|
||||
"id": "1",
|
||||
"is_system_action_type": true,
|
||||
"minimum_license_required": "gold",
|
||||
"name": "name",
|
||||
"supported_feature_ids": Array [
|
||||
"alerting",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
enabled_in_config: true,
|
||||
enabled_in_license: true,
|
||||
supported_feature_ids: ['alerting'],
|
||||
minimum_license_required: 'gold',
|
||||
is_system_action_type: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('passes feature_id if provided as query parameter', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
listTypesWithSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector_types"`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
minimumLicenseRequired: 'gold' as LicenseType,
|
||||
isSystemActionType: false,
|
||||
},
|
||||
];
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ actionsClient },
|
||||
{
|
||||
query: {
|
||||
feature_id: 'alerting',
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"enabled_in_config": true,
|
||||
"enabled_in_license": true,
|
||||
"id": "1",
|
||||
"is_system_action_type": false,
|
||||
"minimum_license_required": "gold",
|
||||
"name": "name",
|
||||
"supported_feature_ids": Array [
|
||||
"alerting",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
|
||||
expect(actionsClient.listTypes).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.listTypes.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"featureId": "alerting",
|
||||
"includeSystemActionTypes": true,
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
enabled_in_config: true,
|
||||
enabled_in_license: true,
|
||||
supported_feature_ids: ['alerting'],
|
||||
minimum_license_required: 'gold',
|
||||
is_system_action_type: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures the license allows listing action types', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
listTypesWithSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector_types"`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
minimumLicenseRequired: 'gold' as LicenseType,
|
||||
isSystemActionType: false,
|
||||
},
|
||||
];
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ actionsClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
|
||||
});
|
||||
|
||||
it('ensures the license check prevents listing action types', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
(verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => {
|
||||
throw new Error('OMG');
|
||||
});
|
||||
|
||||
listTypesWithSystemRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector_types"`);
|
||||
|
||||
const listTypes = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'name',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
supportedFeatureIds: ['alerting'],
|
||||
minimumLicenseRequired: 'gold' as LicenseType,
|
||||
isSystemActionType: false,
|
||||
},
|
||||
];
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.listTypes.mockResolvedValueOnce(listTypes);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ actionsClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
|
||||
|
||||
expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 { ConnectorTypesResponseV1 } from '../../../../common/routes/connector/response';
|
||||
import {
|
||||
connectorTypesQuerySchemaV1,
|
||||
ConnectorTypesRequestQueryV1,
|
||||
} from '../../../../common/routes/connector/apis/connector_types';
|
||||
import { ActionsRequestHandlerContext } from '../../../types';
|
||||
import { INTERNAL_BASE_ACTION_API_PATH } from '../../../../common';
|
||||
import { ILicenseState } from '../../../lib';
|
||||
import { verifyAccessAndContext } from '../../verify_access_and_context';
|
||||
import { transformListTypesResponseV1 } from '../list_types/transforms';
|
||||
|
||||
export const listTypesWithSystemRoute = (
|
||||
router: IRouter<ActionsRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ACTION_API_PATH}/connector_types`,
|
||||
validate: {
|
||||
query: connectorTypesQuerySchemaV1,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const actionsClient = (await context.actions).getActionsClient();
|
||||
|
||||
// Assert versioned inputs
|
||||
const query: ConnectorTypesRequestQueryV1 = req.query;
|
||||
|
||||
const connectorTypes = await actionsClient.listTypes({
|
||||
featureId: query?.feature_id,
|
||||
includeSystemActionTypes: true,
|
||||
});
|
||||
|
||||
const responseBody: ConnectorTypesResponseV1[] =
|
||||
transformListTypesResponseV1(connectorTypes);
|
||||
|
||||
return res.ok({ body: responseBody });
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -24,6 +24,10 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
describe('executeActionRoute', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('executes an action with proper parameters', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
@ -167,4 +171,35 @@ describe('executeActionRoute', () => {
|
|||
|
||||
expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
|
||||
});
|
||||
|
||||
it('returns a bad request for system connectors', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ actionsClient },
|
||||
{
|
||||
body: {
|
||||
params: {},
|
||||
},
|
||||
params: {
|
||||
id: 'system-connector-.test-connector',
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
executeActionRoute(router, licenseState);
|
||||
|
||||
const [_, handler] = router.post.mock.calls[0];
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(actionsClient.execute).not.toHaveBeenCalled();
|
||||
expect(res.ok).not.toHaveBeenCalled();
|
||||
expect(res.badRequest).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,12 +49,18 @@ export const executeActionRoute = (
|
|||
const actionsClient = (await context.actions).getActionsClient();
|
||||
const { params } = req.body;
|
||||
const { id } = req.params;
|
||||
|
||||
if (actionsClient.isSystemAction(id)) {
|
||||
return res.badRequest({ body: 'Execution of system action is not allowed' });
|
||||
}
|
||||
|
||||
const body: ActionTypeExecutorResult<unknown> = await actionsClient.execute({
|
||||
params,
|
||||
actionId: id,
|
||||
source: asHttpRequestExecutionSource(req),
|
||||
relatedSavedObjects: [],
|
||||
});
|
||||
|
||||
return body
|
||||
? res.ok({
|
||||
body: rewriteBodyRes(body),
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
import { IRouter } from '@kbn/core/server';
|
||||
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
import { getAllConnectorsRoute } from './connector/get_all';
|
||||
import { getAllConnectorsIncludingSystemRoute } from './connector/get_all_system';
|
||||
import { listTypesRoute } from './connector/list_types';
|
||||
import { listTypesWithSystemRoute } from './connector/list_types_system';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { ActionsRequestHandlerContext } from '../types';
|
||||
import { createActionRoute } from './create';
|
||||
|
@ -45,4 +47,6 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
getGlobalExecutionKPIRoute(router, licenseState);
|
||||
|
||||
getOAuthAccessToken(router, licenseState, actionsConfigUtils);
|
||||
getAllConnectorsIncludingSystemRoute(router, licenseState);
|
||||
listTypesWithSystemRoute(router, licenseState);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { RRuleParams } from './rrule_type';
|
||||
|
||||
export enum MaintenanceWindowStatus {
|
||||
|
@ -13,14 +14,6 @@ export enum MaintenanceWindowStatus {
|
|||
Finished = 'finished',
|
||||
Archived = 'archived',
|
||||
}
|
||||
|
||||
export const filterStateStore = {
|
||||
APP_STATE: 'appState',
|
||||
GLOBAL_STATE: 'globalState',
|
||||
} as const;
|
||||
|
||||
export type FilterStateStore = typeof filterStateStore[keyof typeof filterStateStore];
|
||||
|
||||
export interface MaintenanceWindowModificationMetadata {
|
||||
createdBy: string | null;
|
||||
updatedBy: string | null;
|
||||
|
|
|
@ -5,10 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { filterStateStore } from './constants/latest';
|
||||
export type { FilterStateStore } from './constants/latest';
|
||||
export { alertsFilterQuerySchema } from './schemas/latest';
|
||||
|
||||
export { filterStateStore as filterStateStoreV1 } from './constants/v1';
|
||||
export type { FilterStateStore as FilterStateStoreV1 } from './constants/v1';
|
||||
export { alertsFilterQuerySchema as alertsFilterQuerySchemaV1 } from './schemas/v1';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { filterStateStore } from '..';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
export const alertsFilterQuerySchema = schema.object({
|
||||
kql: schema.string(),
|
||||
|
@ -17,8 +17,8 @@ export const alertsFilterQuerySchema = schema.object({
|
|||
$state: schema.maybe(
|
||||
schema.object({
|
||||
store: schema.oneOf([
|
||||
schema.literal(filterStateStore.APP_STATE),
|
||||
schema.literal(filterStateStore.GLOBAL_STATE),
|
||||
schema.literal(FilterStateStore.APP_STATE),
|
||||
schema.literal(FilterStateStore.GLOBAL_STATE),
|
||||
]),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -20,7 +20,7 @@ export const ruleSnoozeScheduleSchema = schema.object({
|
|||
});
|
||||
|
||||
const ruleActionSchema = schema.object({
|
||||
group: schema.string(),
|
||||
group: schema.maybe(schema.string()),
|
||||
id: schema.string(),
|
||||
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
|
|
|
@ -46,7 +46,7 @@ export const actionAlertsFilterSchema = schema.object({
|
|||
|
||||
export const actionSchema = schema.object({
|
||||
uuid: schema.maybe(schema.string()),
|
||||
group: schema.string(),
|
||||
group: schema.maybe(schema.string()),
|
||||
id: schema.string(),
|
||||
actionTypeId: schema.maybe(schema.string()),
|
||||
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
|
||||
|
|
|
@ -43,11 +43,6 @@ export const ruleExecutionStatusWarningReason = {
|
|||
MAX_QUEUED_ACTIONS: 'maxQueuedActions',
|
||||
} as const;
|
||||
|
||||
export const filterStateStore = {
|
||||
APP_STATE: 'appState',
|
||||
GLOBAL_STATE: 'globalState',
|
||||
} as const;
|
||||
|
||||
export type RuleNotifyWhen = typeof ruleNotifyWhen[keyof typeof ruleNotifyWhen];
|
||||
export type RuleLastRunOutcomeValues =
|
||||
typeof ruleLastRunOutcomeValues[keyof typeof ruleLastRunOutcomeValues];
|
||||
|
@ -57,4 +52,3 @@ export type RuleExecutionStatusErrorReason =
|
|||
typeof ruleExecutionStatusErrorReason[keyof typeof ruleExecutionStatusErrorReason];
|
||||
export type RuleExecutionStatusWarningReason =
|
||||
typeof ruleExecutionStatusWarningReason[keyof typeof ruleExecutionStatusWarningReason];
|
||||
export type FilterStateStore = typeof filterStateStore[keyof typeof filterStateStore];
|
||||
|
|
|
@ -11,7 +11,6 @@ export {
|
|||
ruleExecutionStatusValues,
|
||||
ruleExecutionStatusErrorReason,
|
||||
ruleExecutionStatusWarningReason,
|
||||
filterStateStore,
|
||||
} from './constants/latest';
|
||||
|
||||
export type {
|
||||
|
@ -20,7 +19,6 @@ export type {
|
|||
RuleExecutionStatusValues,
|
||||
RuleExecutionStatusErrorReason,
|
||||
RuleExecutionStatusWarningReason,
|
||||
FilterStateStore,
|
||||
} from './constants/latest';
|
||||
|
||||
export {
|
||||
|
@ -29,7 +27,6 @@ export {
|
|||
ruleExecutionStatusValues as ruleExecutionStatusValuesV1,
|
||||
ruleExecutionStatusErrorReason as ruleExecutionStatusErrorReasonV1,
|
||||
ruleExecutionStatusWarningReason as ruleExecutionStatusWarningReasonV1,
|
||||
filterStateStore as filterStateStoreV1,
|
||||
} from './constants/v1';
|
||||
|
||||
export type {
|
||||
|
@ -38,5 +35,4 @@ export type {
|
|||
RuleExecutionStatusValues as RuleExecutionStatusValuesV1,
|
||||
RuleExecutionStatusErrorReason as RuleExecutionStatusErrorReasonV1,
|
||||
RuleExecutionStatusWarningReason as RuleExecutionStatusWarningReasonV1,
|
||||
FilterStateStore as FilterStateStoreV1,
|
||||
} from './constants/v1';
|
||||
|
|
|
@ -66,12 +66,13 @@ const actionAlertsFilterSchema = schema.object({
|
|||
|
||||
const actionSchema = schema.object({
|
||||
uuid: schema.maybe(schema.string()),
|
||||
group: schema.string(),
|
||||
group: schema.maybe(schema.string()),
|
||||
id: schema.string(),
|
||||
connector_type_id: schema.string(),
|
||||
params: actionParamsSchema,
|
||||
frequency: schema.maybe(actionFrequencySchema),
|
||||
alerts_filter: schema.maybe(actionAlertsFilterSchema),
|
||||
use_alert_data_for_template: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const ruleExecutionStatusSchema = schema.object({
|
||||
|
|
|
@ -122,6 +122,16 @@ export interface RuleAction {
|
|||
useAlertDataForTemplate?: boolean;
|
||||
}
|
||||
|
||||
export interface RuleSystemAction {
|
||||
uuid?: string;
|
||||
id: string;
|
||||
actionTypeId: string;
|
||||
params: RuleActionParams;
|
||||
}
|
||||
|
||||
export type RuleActionKey = keyof RuleAction;
|
||||
export type RuleSystemActionKey = keyof RuleSystemAction;
|
||||
|
||||
export interface RuleLastRun {
|
||||
outcome: RuleLastRunOutcomes;
|
||||
outcomeOrder?: number;
|
||||
|
@ -155,6 +165,7 @@ export interface Rule<Params extends RuleTypeParams = never> {
|
|||
consumer: string;
|
||||
schedule: IntervalSchedule;
|
||||
actions: RuleAction[];
|
||||
systemActions?: RuleSystemAction[];
|
||||
params: Params;
|
||||
mapped_params?: MappedParams;
|
||||
scheduledTaskId?: string | null;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { filterStateStore } from '../constants';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
export const alertsFilterQuerySchema = schema.object({
|
||||
kql: schema.string(),
|
||||
|
@ -17,8 +17,8 @@ export const alertsFilterQuerySchema = schema.object({
|
|||
$state: schema.maybe(
|
||||
schema.object({
|
||||
store: schema.oneOf([
|
||||
schema.literal(filterStateStore.APP_STATE),
|
||||
schema.literal(filterStateStore.GLOBAL_STATE),
|
||||
schema.literal(FilterStateStore.APP_STATE),
|
||||
schema.literal(FilterStateStore.GLOBAL_STATE),
|
||||
]),
|
||||
})
|
||||
),
|
||||
|
|
|
@ -17,8 +17,3 @@ export const maintenanceWindowCategoryIdTypes = {
|
|||
SECURITY_SOLUTION: 'securitySolution',
|
||||
MANAGEMENT: 'management',
|
||||
} as const;
|
||||
|
||||
export const filterStateStore = {
|
||||
APP_STATE: 'appState',
|
||||
GLOBAL_STATE: 'globalState',
|
||||
} as const;
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../../../../../common';
|
||||
import { getMockMaintenanceWindow } from '../../../../data/maintenance_window/test_helpers';
|
||||
import type { MaintenanceWindow } from '../../types';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
|
@ -168,7 +169,7 @@ describe('MaintenanceWindowClient - create', () => {
|
|||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
|
@ -281,7 +282,7 @@ describe('MaintenanceWindowClient - create', () => {
|
|||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from '../../../../../common';
|
||||
import { getMockMaintenanceWindow } from '../../../../data/maintenance_window/test_helpers';
|
||||
import type { MaintenanceWindow } from '../../types';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
|
@ -260,7 +261,7 @@ describe('MaintenanceWindowClient - update', () => {
|
|||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
|
@ -407,7 +408,7 @@ describe('MaintenanceWindowClient - update', () => {
|
|||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
|
|
|
@ -26,7 +26,15 @@ export const transformMaintenanceWindowToMaintenanceWindowAttributes = (
|
|||
? { categoryIds: maintenanceWindow.categoryIds }
|
||||
: {}),
|
||||
...(maintenanceWindow.scopedQuery !== undefined
|
||||
? { scopedQuery: maintenanceWindow.scopedQuery }
|
||||
? maintenanceWindow?.scopedQuery == null
|
||||
? { scopedQuery: maintenanceWindow?.scopedQuery }
|
||||
: {
|
||||
scopedQuery: {
|
||||
filters: maintenanceWindow?.scopedQuery?.filters ?? [],
|
||||
kql: maintenanceWindow?.scopedQuery?.kql ?? '',
|
||||
dsl: maintenanceWindow?.scopedQuery?.dsl ?? '',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -42,8 +42,3 @@ export const ruleExecutionStatusWarningReason = {
|
|||
MAX_ALERTS: 'maxAlerts',
|
||||
MAX_QUEUED_ACTIONS: 'maxQueuedActions',
|
||||
} as const;
|
||||
|
||||
export const filterStateStore = {
|
||||
APP_STATE: 'appState',
|
||||
GLOBAL_STATE: 'globalState',
|
||||
} as const;
|
||||
|
|
|
@ -27,6 +27,7 @@ import { fromKueryExpression, nodeTypes } from '@kbn/es-query';
|
|||
import { RecoveredActionGroup } from '../../../../../common';
|
||||
import { DefaultRuleAggregationResult } from '../../../../routes/rule/apis/aggregate/types';
|
||||
import { defaultRuleAggregationFactory } from '.';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
|
@ -58,11 +59,13 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
maxScheduledPerMinute: 1000,
|
||||
internalSavedObjectsRepository,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
isSystemAction: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -18,6 +18,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';
|
||||
|
@ -28,12 +29,17 @@ import {
|
|||
enabledRuleForBulkOps1,
|
||||
enabledRuleForBulkOps2,
|
||||
enabledRuleForBulkOps3,
|
||||
returnedRuleForBulkDelete1,
|
||||
returnedRuleForBulkDelete2,
|
||||
returnedRuleForBulkDelete3,
|
||||
returnedRuleForBulkOps1,
|
||||
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';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
|
@ -84,6 +90,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
isSystemAction: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
|
@ -109,11 +117,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()
|
||||
|
@ -165,6 +174,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 () => {
|
||||
|
@ -196,7 +247,7 @@ describe('bulkDelete', () => {
|
|||
expect.anything()
|
||||
);
|
||||
expect(result).toStrictEqual({
|
||||
rules: [returnedRuleForBulkDelete1, returnedRuleForBulkDelete3],
|
||||
rules: [returnedRuleForBulkOps1, returnedRuleForBulkOps3],
|
||||
errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 500 }],
|
||||
total: 2,
|
||||
taskIdsFailedToBeDeleted: [],
|
||||
|
@ -260,7 +311,7 @@ describe('bulkDelete', () => {
|
|||
expect.anything()
|
||||
);
|
||||
expect(result).toStrictEqual({
|
||||
rules: [returnedRuleForBulkDelete1],
|
||||
rules: [returnedRuleForBulkOps1],
|
||||
errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 409 }],
|
||||
total: 2,
|
||||
taskIdsFailedToBeDeleted: [],
|
||||
|
@ -318,7 +369,7 @@ describe('bulkDelete', () => {
|
|||
expect.anything()
|
||||
);
|
||||
expect(result).toStrictEqual({
|
||||
rules: [returnedRuleForBulkDelete1, returnedRuleForBulkDelete2],
|
||||
rules: [returnedRuleForBulkOps1, returnedRuleForBulkOps2],
|
||||
errors: [],
|
||||
total: 2,
|
||||
taskIdsFailedToBeDeleted: [],
|
||||
|
|
|
@ -50,6 +50,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' });
|
||||
|
@ -96,13 +97,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);
|
||||
|
|
|
@ -31,14 +31,20 @@ import {
|
|||
savedObjectWith500Error,
|
||||
disabledRuleForBulkDisable1,
|
||||
disabledRuleForBulkDisable2,
|
||||
returnedRuleForBulkDisableWithActions1,
|
||||
returnedRuleForBulkDisableWithActions2,
|
||||
enabledRuleForBulkOps1,
|
||||
enabledRuleForBulkOps2,
|
||||
disabledRuleForBulkOpsWithActions1,
|
||||
disabledRuleForBulkOpsWithActions2,
|
||||
returnedRuleForBulkDisable1,
|
||||
returnedRuleForBulkDisable2,
|
||||
siemRuleForBulkOps1,
|
||||
siemRuleForBulkOps2,
|
||||
} from '../../../../rules_client/tests/test_helpers';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
|
@ -96,6 +102,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
isSystemAction: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
|
@ -110,6 +118,8 @@ setGlobalDate();
|
|||
|
||||
describe('bulkDisableRules', () => {
|
||||
let rulesClient: RulesClient;
|
||||
let actionsClient: jest.Mocked<ActionsClient>;
|
||||
|
||||
const mockCreatePointInTimeFinderAsInternalUser = (
|
||||
response = { saved_objects: [enabledRule1, enabledRule2] }
|
||||
) => {
|
||||
|
@ -145,6 +155,9 @@ describe('bulkDisableRules', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
rulesClient = new RulesClient(rulesClientParams);
|
||||
actionsClient = (await rulesClientParams.getActionsClient()) as jest.Mocked<ActionsClient>;
|
||||
rulesClientParams.getActionsClient.mockResolvedValue(actionsClient);
|
||||
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
|
@ -155,6 +168,7 @@ describe('bulkDisableRules', () => {
|
|||
resultedActions: [],
|
||||
resultedReferences: [],
|
||||
});
|
||||
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action:id');
|
||||
});
|
||||
|
||||
test('should disable two rule', async () => {
|
||||
|
@ -190,6 +204,38 @@ describe('bulkDisableRules', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should disable two rule and return right actions', async () => {
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [disabledRuleForBulkOpsWithActions1, disabledRuleForBulkOpsWithActions2],
|
||||
});
|
||||
|
||||
const result = await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'id1',
|
||||
attributes: expect.objectContaining({
|
||||
enabled: false,
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'id2',
|
||||
attributes: expect.objectContaining({
|
||||
enabled: false,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
{ overwrite: true }
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
errors: [],
|
||||
rules: [returnedRuleForBulkDisableWithActions1, returnedRuleForBulkDisableWithActions2],
|
||||
total: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('should call untrack alert if untrack is true', async () => {
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2],
|
||||
|
@ -683,25 +729,25 @@ describe('bulkDisableRules', () => {
|
|||
await rulesClient.bulkDisableRules({ filter: 'fake_filter' });
|
||||
|
||||
expect(migrateLegacyActions).toHaveBeenCalledTimes(4);
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(1, expect.any(Object), {
|
||||
attributes: enabledRuleForBulkOps1.attributes,
|
||||
ruleId: enabledRuleForBulkOps1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(2, expect.any(Object), {
|
||||
attributes: enabledRuleForBulkOps2.attributes,
|
||||
ruleId: enabledRuleForBulkOps2.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(3, expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRuleForBulkOps1.id,
|
||||
actions: [],
|
||||
references: [],
|
||||
});
|
||||
expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), {
|
||||
expect(migrateLegacyActions).toHaveBeenNthCalledWith(4, expect.any(Object), {
|
||||
attributes: expect.objectContaining({ consumer: AlertConsumers.SIEM }),
|
||||
ruleId: siemRuleForBulkOps2.id,
|
||||
actions: [],
|
||||
|
|
|
@ -51,6 +51,7 @@ export const bulkDisableRules = async <Params extends RuleParams>(
|
|||
}
|
||||
|
||||
const { ids, filter, untrack = false } = options;
|
||||
const actionsClient = await context.getActionsClient();
|
||||
|
||||
const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter);
|
||||
const authorizationFilter = await getAuthorizationFilter(context, { action: 'DISABLE' });
|
||||
|
@ -94,13 +95,17 @@ export const bulkDisableRules = async <Params extends RuleParams>(
|
|||
// fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject
|
||||
// when we are doing the bulk disable 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);
|
||||
|
|
|
@ -27,7 +27,6 @@ import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server'
|
|||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
import { NormalizedAlertAction } from '../../../../rules_client/types';
|
||||
import {
|
||||
enabledRule1,
|
||||
enabledRule2,
|
||||
|
@ -36,6 +35,11 @@ import {
|
|||
} from '../../../../rules_client/tests/test_helpers';
|
||||
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 { ConnectorAdapter } from '../../../../connector_adapters/types';
|
||||
import { RuleAttributes } from '../../../../data/rule/types';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import { bulkEditOperationsSchema } from './schemas';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
|
||||
jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => {
|
||||
|
@ -104,6 +108,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: isAuthenticationTypeApiKeyMock,
|
||||
getAuthenticationAPIKey: getAuthenticationApiKeyMock,
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
isSystemAction: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
|
@ -245,14 +251,17 @@ describe('bulkEdit()', () => {
|
|||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
validLegacyConsumers: [],
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
},
|
||||
validLegacyConsumers: [],
|
||||
});
|
||||
|
||||
(migrateLegacyActions as jest.Mock).mockResolvedValue(migrateLegacyActionsMock);
|
||||
|
||||
rulesClientParams.isSystemAction.mockImplementation((id: string) => id === 'system_action-id');
|
||||
actionsClient.isSystemAction.mockImplementation((id: string) => id === 'system_action-id');
|
||||
});
|
||||
|
||||
describe('tags operations', () => {
|
||||
|
@ -537,6 +546,14 @@ describe('bulkEdit()', () => {
|
|||
});
|
||||
|
||||
describe('actions operations', () => {
|
||||
const connectorAdapter: ConnectorAdapter = {
|
||||
connectorTypeId: '.test',
|
||||
ruleActionParamsSchema: schema.object({ foo: schema.string() }),
|
||||
buildActionParams: jest.fn(),
|
||||
};
|
||||
|
||||
rulesClientParams.connectorAdapterRegistry.register(connectorAdapter);
|
||||
|
||||
beforeEach(() => {
|
||||
mockCreatePointInTimeFinderAsInternalUser({
|
||||
saved_objects: [existingDecryptedRule],
|
||||
|
@ -546,7 +563,7 @@ describe('bulkEdit()', () => {
|
|||
test('should add uuid to new actions', async () => {
|
||||
const existingAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
|
@ -555,9 +572,10 @@ describe('bulkEdit()', () => {
|
|||
params: {},
|
||||
uuid: '111',
|
||||
};
|
||||
|
||||
const newAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
|
@ -565,9 +583,10 @@ describe('bulkEdit()', () => {
|
|||
id: '2',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const newAction2 = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
|
@ -586,10 +605,12 @@ describe('bulkEdit()', () => {
|
|||
{
|
||||
...existingAction,
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-0',
|
||||
},
|
||||
{
|
||||
...newAction,
|
||||
actionRef: 'action_1',
|
||||
actionTypeId: 'test-1',
|
||||
uuid: '222',
|
||||
},
|
||||
],
|
||||
|
@ -616,7 +637,7 @@ describe('bulkEdit()', () => {
|
|||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [existingAction, newAction, newAction2] as NormalizedAlertAction[],
|
||||
value: [existingAction, newAction, newAction2],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -678,7 +699,11 @@ describe('bulkEdit()', () => {
|
|||
...existingRule.attributes.executionStatus,
|
||||
lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate),
|
||||
},
|
||||
actions: [existingAction, { ...newAction, uuid: '222' }],
|
||||
actions: [
|
||||
{ ...existingAction, actionTypeId: 'test-0' },
|
||||
{ ...newAction, uuid: '222', actionTypeId: 'test-1' },
|
||||
],
|
||||
systemActions: [],
|
||||
id: existingRule.id,
|
||||
snoozeSchedule: [],
|
||||
});
|
||||
|
@ -743,7 +768,6 @@ describe('bulkEdit()', () => {
|
|||
async executor() {
|
||||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
|
@ -753,11 +777,13 @@ describe('bulkEdit()', () => {
|
|||
mappings: { fieldMap: { field: { type: 'keyword', required: false } } },
|
||||
shouldWrite: true,
|
||||
},
|
||||
category: 'test',
|
||||
validLegacyConsumers: [],
|
||||
});
|
||||
|
||||
const existingAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
|
@ -780,7 +806,7 @@ describe('bulkEdit()', () => {
|
|||
};
|
||||
const newAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
|
@ -834,7 +860,7 @@ describe('bulkEdit()', () => {
|
|||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [existingAction, newAction] as NormalizedAlertAction[],
|
||||
value: [existingAction, newAction],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -910,8 +936,651 @@ describe('bulkEdit()', () => {
|
|||
],
|
||||
id: existingRule.id,
|
||||
snoozeSchedule: [],
|
||||
systemActions: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should add system and default actions', async () => {
|
||||
const defaultAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const systemAction = {
|
||||
id: 'system_action-id',
|
||||
params: {},
|
||||
};
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
...existingRule,
|
||||
attributes: {
|
||||
...existingRule.attributes,
|
||||
actions: [
|
||||
{
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
params: {},
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-1',
|
||||
uuid: '222',
|
||||
},
|
||||
{
|
||||
params: {},
|
||||
actionRef: 'system_action:system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
uuid: '222',
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: '1',
|
||||
actionTypeId: 'test-1',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'test default connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
},
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [defaultAction, systemAction],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
...existingRule,
|
||||
attributes: {
|
||||
...existingRule.attributes,
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-1',
|
||||
frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null },
|
||||
group: 'default',
|
||||
params: {},
|
||||
uuid: '103',
|
||||
},
|
||||
{
|
||||
actionRef: 'system_action:system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
params: {},
|
||||
uuid: '104',
|
||||
},
|
||||
],
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
apiKeyCreatedByUser: null,
|
||||
meta: { versionApiKeyLastmodified: 'v8.2.0' },
|
||||
name: 'my rule name',
|
||||
enabled: false,
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
tags: ['foo'],
|
||||
revision: 1,
|
||||
},
|
||||
references: [{ id: '1', name: 'action_0', type: 'action' }],
|
||||
},
|
||||
],
|
||||
{ overwrite: true }
|
||||
);
|
||||
|
||||
expect(result.rules[0]).toEqual({
|
||||
...omit(existingRule.attributes, 'legacyId'),
|
||||
createdAt: new Date(existingRule.attributes.createdAt),
|
||||
updatedAt: new Date(existingRule.attributes.updatedAt),
|
||||
executionStatus: {
|
||||
...existingRule.attributes.executionStatus,
|
||||
lastExecutionDate: new Date(existingRule.attributes.executionStatus.lastExecutionDate),
|
||||
},
|
||||
actions: [{ ...defaultAction, actionTypeId: 'test-1', uuid: '222' }],
|
||||
systemActions: [{ ...systemAction, actionTypeId: 'test-2', uuid: '222' }],
|
||||
id: existingRule.id,
|
||||
snoozeSchedule: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('should construct the refs correctly and persist the actions correctly', async () => {
|
||||
const defaultAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const systemAction = {
|
||||
id: 'system_action-id',
|
||||
params: {},
|
||||
};
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
...existingRule,
|
||||
attributes: {
|
||||
...existingRule.attributes,
|
||||
actions: [
|
||||
{
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
params: {},
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-1',
|
||||
uuid: '222',
|
||||
},
|
||||
{
|
||||
params: {},
|
||||
actionRef: 'system_action:system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
uuid: '222',
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: '1',
|
||||
actionTypeId: 'test-1',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'test default connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
},
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [defaultAction, systemAction],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const rule = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0] as Array<
|
||||
SavedObject<RuleAttributes>
|
||||
>;
|
||||
|
||||
expect(rule[0].attributes.actions).toEqual([
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-1',
|
||||
frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null },
|
||||
group: 'default',
|
||||
params: {},
|
||||
uuid: '105',
|
||||
},
|
||||
{
|
||||
actionRef: 'system_action:system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
params: {},
|
||||
uuid: '106',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should transforms the actions correctly', async () => {
|
||||
const defaultAction = {
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const systemAction = {
|
||||
id: 'system_action-id',
|
||||
params: {},
|
||||
};
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
...existingRule,
|
||||
attributes: {
|
||||
...existingRule.attributes,
|
||||
actions: [
|
||||
{
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
params: {},
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-1',
|
||||
uuid: '222',
|
||||
},
|
||||
{
|
||||
params: {},
|
||||
actionRef: 'system_action:system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
uuid: '222',
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: '1',
|
||||
actionTypeId: 'test-1',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'test default connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: false,
|
||||
},
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [defaultAction, systemAction],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.rules[0].actions).toEqual([
|
||||
{ ...defaultAction, actionTypeId: 'test-1', uuid: '222' },
|
||||
]);
|
||||
expect(result.rules[0].systemActions).toEqual([
|
||||
{ ...systemAction, actionTypeId: 'test-2', uuid: '222' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an error if the action does not have the right attributes', async () => {
|
||||
const action = {
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: {},
|
||||
};
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(false);
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [action],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"message": "Error validating bulk edit rules operations - [0.group]: expected value of type [string] but got [undefined]",
|
||||
"rule": Object {
|
||||
"id": "1",
|
||||
"name": "my rule name",
|
||||
},
|
||||
},
|
||||
],
|
||||
"rules": Array [],
|
||||
"skipped": Array [],
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw an error if the system action contains the group', async () => {
|
||||
const action = {
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: {},
|
||||
group: 'default',
|
||||
};
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [action],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Error validating bulk edit rules operations - [0.group]: definition for this key is missing',
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: [],
|
||||
skipped: [],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the system action contains the frequency', async () => {
|
||||
const action = {
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: {},
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
};
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [action],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Error validating bulk edit rules operations - [0.frequency]: definition for this key is missing',
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: [],
|
||||
skipped: [],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the system action contains the alertsFilter', async () => {
|
||||
const action = {
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: {},
|
||||
alertsFilter: {
|
||||
query: { kql: 'test:1', filters: [] },
|
||||
},
|
||||
};
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [action],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Error validating bulk edit rules operations - [0.alertsFilter]: definition for this key is missing',
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: [],
|
||||
skipped: [],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the same system action is used twice', async () => {
|
||||
const action = {
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: {},
|
||||
};
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const res = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [action, action],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message: 'Cannot use the same system action twice',
|
||||
rule: {
|
||||
id: '1',
|
||||
name: 'my rule name',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: [],
|
||||
skipped: [],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the default action does not contain the group', async () => {
|
||||
const action = {
|
||||
id: '1',
|
||||
params: {},
|
||||
};
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(false);
|
||||
|
||||
await expect(
|
||||
rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [
|
||||
{
|
||||
field: 'actions',
|
||||
operation: 'add',
|
||||
value: [action],
|
||||
},
|
||||
],
|
||||
})
|
||||
).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Object {
|
||||
"message": "Error validating bulk edit rules operations - [0.group]: expected value of type [string] but got [undefined]",
|
||||
"rule": Object {
|
||||
"id": "1",
|
||||
"name": "my rule name",
|
||||
},
|
||||
},
|
||||
],
|
||||
"rules": Array [],
|
||||
"skipped": Array [],
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('index pattern operations', () => {
|
||||
|
@ -969,7 +1638,13 @@ describe('bulkEdit()', () => {
|
|||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [],
|
||||
operations: [
|
||||
{
|
||||
field: 'tags',
|
||||
operation: 'add',
|
||||
value: ['test-tag'],
|
||||
},
|
||||
],
|
||||
paramsModifier,
|
||||
});
|
||||
|
||||
|
@ -1038,7 +1713,13 @@ describe('bulkEdit()', () => {
|
|||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [],
|
||||
operations: [
|
||||
{
|
||||
field: 'tags',
|
||||
operation: 'add',
|
||||
value: ['test-tag'],
|
||||
},
|
||||
],
|
||||
paramsModifier,
|
||||
});
|
||||
|
||||
|
@ -1063,6 +1744,9 @@ describe('bulkEdit()', () => {
|
|||
});
|
||||
|
||||
test('should skip operation when params modifiers does not modify index pattern array', async () => {
|
||||
const originalValidate = bulkEditOperationsSchema.validate;
|
||||
bulkEditOperationsSchema.validate = jest.fn();
|
||||
|
||||
paramsModifier.mockResolvedValue({
|
||||
modifiedParams: {
|
||||
index: ['test-1', 'test-2'],
|
||||
|
@ -1081,6 +1765,8 @@ describe('bulkEdit()', () => {
|
|||
|
||||
expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(0);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(0);
|
||||
|
||||
bulkEditOperationsSchema.validate = originalValidate;
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2325,8 +3011,8 @@ describe('bulkEdit()', () => {
|
|||
async executor() {
|
||||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
category: 'test',
|
||||
validLegacyConsumers: [],
|
||||
});
|
||||
|
||||
|
@ -2371,8 +3057,8 @@ describe('bulkEdit()', () => {
|
|||
async executor() {
|
||||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
category: 'test',
|
||||
validLegacyConsumers: [],
|
||||
});
|
||||
|
||||
|
@ -2411,7 +3097,13 @@ describe('bulkEdit()', () => {
|
|||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [],
|
||||
operations: [
|
||||
{
|
||||
field: 'tags',
|
||||
operation: 'add',
|
||||
value: ['test-1'],
|
||||
},
|
||||
],
|
||||
paramsModifier: async (params) => {
|
||||
params.index = ['test-index-*'];
|
||||
|
||||
|
@ -2546,6 +3238,49 @@ describe('bulkEdit()', () => {
|
|||
|
||||
expect(validateScheduleLimit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not validate scheduling on system actions', async () => {
|
||||
mockCreatePointInTimeFinderAsInternalUser({
|
||||
saved_objects: [
|
||||
{
|
||||
...existingDecryptedRule,
|
||||
attributes: {
|
||||
...existingDecryptedRule.attributes,
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {},
|
||||
uuid: '111',
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
] as any,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
operations: [
|
||||
{
|
||||
field: 'schedule',
|
||||
operation: 'set',
|
||||
value: { interval: '10m' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.rules).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('paramsModifier', () => {
|
||||
|
@ -2579,7 +3314,13 @@ describe('bulkEdit()', () => {
|
|||
|
||||
const result = await rulesClient.bulkEdit({
|
||||
filter: '',
|
||||
operations: [],
|
||||
operations: [
|
||||
{
|
||||
field: 'tags',
|
||||
operation: 'add',
|
||||
value: ['test-1'],
|
||||
},
|
||||
],
|
||||
paramsModifier: async (params) => {
|
||||
params.index = ['test-index-*'];
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
SavedObjectsFindResult,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { validateSystemActions } from '../../../../lib/validate_system_actions';
|
||||
import { RuleAction, RuleSystemAction } from '../../../../../common';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { BulkActionSkipResult } from '../../../../../common/bulk_edit';
|
||||
import { RuleTypeRegistry } from '../../../../types';
|
||||
|
@ -32,10 +34,10 @@ import {
|
|||
retryIfBulkEditConflicts,
|
||||
applyBulkEditOperation,
|
||||
buildKueryNodeFilter,
|
||||
injectReferencesIntoActions,
|
||||
getBulkSnooze,
|
||||
getBulkUnsnooze,
|
||||
verifySnoozeScheduleLimit,
|
||||
injectReferencesIntoActions,
|
||||
} from '../../../../rules_client/common';
|
||||
import {
|
||||
alertingAuthorizationFilterOpts,
|
||||
|
@ -56,6 +58,7 @@ import {
|
|||
RuleBulkOperationAggregation,
|
||||
RulesClientContext,
|
||||
NormalizedAlertActionWithGeneratedValues,
|
||||
NormalizedAlertAction,
|
||||
} from '../../../../rules_client/types';
|
||||
import { migrateLegacyActions } from '../../../../rules_client/lib';
|
||||
import {
|
||||
|
@ -78,6 +81,10 @@ import {
|
|||
transformRuleDomainToRule,
|
||||
} from '../../transforms';
|
||||
import { validateScheduleLimit, ValidateScheduleLimitResult } from '../get_schedule_frequency';
|
||||
import {
|
||||
bulkEditDefaultActionsSchema,
|
||||
bulkEditSystemActionsSchema,
|
||||
} from './schemas/bulk_edit_rules_option_schemas';
|
||||
|
||||
const isValidInterval = (interval: string | undefined): interval is string => {
|
||||
return interval !== undefined;
|
||||
|
@ -117,6 +124,7 @@ export async function bulkEditRules<Params extends RuleParams>(
|
|||
): Promise<BulkEditResult<Params>> {
|
||||
const queryFilter = (options as BulkEditOptionsFilter<Params>).filter;
|
||||
const ids = (options as BulkEditOptionsIds<Params>).ids;
|
||||
const actionsClient = await context.getActionsClient();
|
||||
|
||||
if (ids && queryFilter) {
|
||||
throw Boom.badRequest(
|
||||
|
@ -231,13 +239,17 @@ export async function bulkEditRules<Params extends RuleParams>(
|
|||
// fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject
|
||||
// when we are doing the bulk create 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);
|
||||
} catch (e) {
|
||||
|
@ -358,7 +370,6 @@ async function bulkEditRulesOcc<Params extends RuleParams>(
|
|||
skipped: [],
|
||||
};
|
||||
}
|
||||
|
||||
const { result, apiKeysToInvalidate } =
|
||||
rules.length > 0
|
||||
? await saveBulkUpdatedRules({
|
||||
|
@ -478,7 +489,8 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
|
|||
logger: context.logger,
|
||||
ruleType: context.ruleTypeRegistry.get(rule.attributes.alertTypeId),
|
||||
references: rule.references,
|
||||
}
|
||||
},
|
||||
context.isSystemAction
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -490,7 +502,7 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
|
|||
context,
|
||||
operations,
|
||||
rule: ruleDomain,
|
||||
ruleActions: ruleActions as RuleDomain['actions'], // TODO (http-versioning) Remove this cast once we fix injectReferencesIntoActions
|
||||
ruleActions,
|
||||
ruleType,
|
||||
});
|
||||
|
||||
|
@ -532,9 +544,9 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
|
|||
);
|
||||
|
||||
const {
|
||||
actions: rawAlertActions,
|
||||
references,
|
||||
params: updatedParams,
|
||||
actions: actionsWithRefs,
|
||||
} = await extractReferences(
|
||||
context,
|
||||
ruleType,
|
||||
|
@ -542,10 +554,13 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
|
|||
validatedMutatedAlertTypeParams
|
||||
);
|
||||
|
||||
const ruleAttributes = transformRuleDomainToRuleAttributes(updatedRule, {
|
||||
legacyId: rule.attributes.legacyId,
|
||||
actionsWithRefs: rawAlertActions,
|
||||
paramsWithRefs: updatedParams as RuleAttributes['params'],
|
||||
const ruleAttributes = transformRuleDomainToRuleAttributes({
|
||||
actionsWithRefs,
|
||||
rule: updatedRule,
|
||||
params: {
|
||||
legacyId: rule.attributes.legacyId,
|
||||
paramsWithRefs: updatedParams as RuleAttributes['params'],
|
||||
},
|
||||
});
|
||||
|
||||
const { apiKeyAttributes } = await prepareApiKeys(
|
||||
|
@ -563,7 +578,7 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleParams>(
|
|||
ruleAttributes,
|
||||
apiKeyAttributes,
|
||||
updatedParams,
|
||||
rawAlertActions,
|
||||
ruleAttributes.actions,
|
||||
username
|
||||
);
|
||||
|
||||
|
@ -621,9 +636,11 @@ async function getUpdatedAttributesFromOperations<Params extends RuleParams>({
|
|||
context: RulesClientContext;
|
||||
operations: BulkEditOperation[];
|
||||
rule: RuleDomain<Params>;
|
||||
ruleActions: RuleDomain['actions'];
|
||||
ruleActions: RuleDomain['actions'] | RuleDomain['systemActions'];
|
||||
ruleType: RuleType;
|
||||
}) {
|
||||
const actionsClient = await context.getActionsClient();
|
||||
|
||||
let updatedRule = cloneDeep(rule);
|
||||
let updatedRuleActions = ruleActions;
|
||||
let hasUpdateApiKeyOperation = false;
|
||||
|
@ -636,21 +653,53 @@ async function getUpdatedAttributesFromOperations<Params extends RuleParams>({
|
|||
// the `isAttributesUpdateSkipped` flag to false.
|
||||
switch (operation.field) {
|
||||
case 'actions': {
|
||||
const systemActions = operation.value.filter((action): action is RuleSystemAction =>
|
||||
actionsClient.isSystemAction(action.id)
|
||||
);
|
||||
if (systemActions.length > 0) {
|
||||
try {
|
||||
bulkEditSystemActionsSchema.validate(systemActions);
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error validating bulk edit rules operations - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultActions = operation.value.filter(
|
||||
(action): action is RuleAction => !actionsClient.isSystemAction(action.id)
|
||||
);
|
||||
if (defaultActions.length > 0) {
|
||||
try {
|
||||
bulkEditDefaultActionsSchema.validate(defaultActions);
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error validating bulk edit rules operations - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { actions: genActions, systemActions: genSystemActions } =
|
||||
await addGeneratedActionValues(defaultActions, systemActions, context);
|
||||
const updatedOperation = {
|
||||
...operation,
|
||||
value: await addGeneratedActionValues(operation.value, context),
|
||||
value: [...genActions, ...genSystemActions],
|
||||
};
|
||||
|
||||
await validateSystemActions({
|
||||
actionsClient,
|
||||
connectorAdapterRegistry: context.connectorAdapterRegistry,
|
||||
systemActions: genSystemActions,
|
||||
});
|
||||
|
||||
try {
|
||||
await validateActions(context, ruleType, {
|
||||
...updatedRule,
|
||||
actions: updatedOperation.value,
|
||||
actions: genActions,
|
||||
systemActions: genSystemActions,
|
||||
});
|
||||
} catch (e) {
|
||||
// If validateActions fails on the first attempt, it may be because of legacy rule-level frequency params
|
||||
updatedRule = await attemptToMigrateLegacyFrequency(
|
||||
context,
|
||||
updatedOperation,
|
||||
operation.field,
|
||||
genActions,
|
||||
updatedRule,
|
||||
ruleType
|
||||
);
|
||||
|
@ -669,23 +718,27 @@ async function getUpdatedAttributesFromOperations<Params extends RuleParams>({
|
|||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'snoozeSchedule': {
|
||||
if (operation.operation === 'set') {
|
||||
const snoozeAttributes = getBulkSnooze<Params>(
|
||||
updatedRule,
|
||||
operation.value as RuleSnoozeSchedule
|
||||
);
|
||||
|
||||
try {
|
||||
verifySnoozeScheduleLimit(snoozeAttributes.snoozeSchedule);
|
||||
} catch (error) {
|
||||
throw Error(`Error updating rule: could not add snooze - ${error.message}`);
|
||||
}
|
||||
|
||||
updatedRule = {
|
||||
...updatedRule,
|
||||
muteAll: snoozeAttributes.muteAll,
|
||||
snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'],
|
||||
};
|
||||
}
|
||||
|
||||
if (operation.operation === 'delete') {
|
||||
const idsToDelete = operation.value && [...operation.value];
|
||||
if (idsToDelete?.length === 0) {
|
||||
|
@ -702,18 +755,25 @@ async function getUpdatedAttributesFromOperations<Params extends RuleParams>({
|
|||
snoozeSchedule: snoozeAttributes.snoozeSchedule as RuleDomain['snoozeSchedule'],
|
||||
};
|
||||
}
|
||||
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'apiKey': {
|
||||
hasUpdateApiKeyOperation = true;
|
||||
isAttributesUpdateSkipped = false;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (operation.field === 'schedule') {
|
||||
validateScheduleOperation(operation.value, updatedRule.actions, rule.id);
|
||||
const defaultActions = updatedRule.actions.filter(
|
||||
(action) => !actionsClient.isSystemAction(action.id)
|
||||
);
|
||||
validateScheduleOperation(operation.value, defaultActions, rule.id);
|
||||
}
|
||||
|
||||
const { modifiedAttributes, isAttributeModified } = applyBulkEditOperation(
|
||||
operation,
|
||||
updatedRule
|
||||
|
@ -923,18 +983,19 @@ async function saveBulkUpdatedRules({
|
|||
|
||||
async function attemptToMigrateLegacyFrequency<Params extends RuleParams>(
|
||||
context: RulesClientContext,
|
||||
operation: BulkEditOperation,
|
||||
operationField: BulkEditOperation['field'],
|
||||
actions: NormalizedAlertAction[],
|
||||
rule: RuleDomain<Params>,
|
||||
ruleType: RuleType
|
||||
) {
|
||||
if (operation.field !== 'actions')
|
||||
if (operationField !== 'actions')
|
||||
throw new Error('Can only perform frequency migration on an action operation');
|
||||
// Try to remove the rule-level frequency params, and then validate actions
|
||||
if (typeof rule.notifyWhen !== 'undefined') rule.notifyWhen = undefined;
|
||||
if (rule.throttle) rule.throttle = undefined;
|
||||
await validateActions(context, ruleType, {
|
||||
...rule,
|
||||
actions: operation.value,
|
||||
actions,
|
||||
});
|
||||
return rule;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { rRuleRequestSchema } from '../../../../r_rule/schemas';
|
||||
import { notifyWhenSchema } from '../../../schemas';
|
||||
import { notifyWhenSchema, actionAlertsFilterSchema } from '../../../schemas';
|
||||
import { validateDuration } from '../../../validation';
|
||||
import { validateSnoozeSchedule } from '../validation';
|
||||
|
||||
|
@ -26,7 +26,7 @@ const bulkEditRuleSnoozeScheduleSchemaWithValidation = schema.object(
|
|||
{ validate: validateSnoozeSchedule }
|
||||
);
|
||||
|
||||
const bulkEditActionSchema = schema.object({
|
||||
const bulkEditDefaultActionSchema = schema.object({
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
|
||||
|
@ -38,8 +38,19 @@ const bulkEditActionSchema = schema.object({
|
|||
notifyWhen: notifyWhenSchema,
|
||||
})
|
||||
),
|
||||
alertsFilter: schema.maybe(actionAlertsFilterSchema),
|
||||
});
|
||||
|
||||
export const bulkEditDefaultActionsSchema = schema.arrayOf(bulkEditDefaultActionSchema);
|
||||
|
||||
export const bulkEditSystemActionSchema = schema.object({
|
||||
id: schema.string(),
|
||||
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export const bulkEditSystemActionsSchema = schema.arrayOf(bulkEditSystemActionSchema);
|
||||
|
||||
const bulkEditTagSchema = schema.object({
|
||||
operation: schema.oneOf([schema.literal('add'), schema.literal('delete'), schema.literal('set')]),
|
||||
field: schema.literal('tags'),
|
||||
|
@ -49,7 +60,7 @@ const bulkEditTagSchema = schema.object({
|
|||
const bulkEditActionsSchema = schema.object({
|
||||
operation: schema.oneOf([schema.literal('add'), schema.literal('set')]),
|
||||
field: schema.literal('actions'),
|
||||
value: schema.arrayOf(bulkEditActionSchema),
|
||||
value: schema.arrayOf(schema.oneOf([bulkEditDefaultActionSchema, bulkEditSystemActionSchema])),
|
||||
});
|
||||
|
||||
const bulkEditScheduleSchema = schema.object({
|
||||
|
|
|
@ -22,6 +22,7 @@ import { AlertingAuthorization } from '../../../../authorization/alerting_author
|
|||
import { alertsServiceMock } from '../../../../alerts_service/alerts_service.mock';
|
||||
import { ALERT_RULE_UUID, ALERT_UUID } from '@kbn/rule-data-utils';
|
||||
import { ConcreteTaskInstance, TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -61,6 +62,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
isSystemAction: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
};
|
||||
|
||||
describe('bulkUntrackAlerts()', () => {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,7 @@ import Semver from 'semver';
|
|||
import Boom from '@hapi/boom';
|
||||
import { SavedObject, SavedObjectsUtils } from '@kbn/core/server';
|
||||
import { withSpan } from '@kbn/apm-utils';
|
||||
import { validateSystemActions } from '../../../../lib/validate_system_actions';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { parseDuration, getRuleCircuitBreakerErrorMessage } from '../../../../../common';
|
||||
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
|
||||
|
@ -24,7 +25,7 @@ import {
|
|||
} from '../../../../rules_client/lib';
|
||||
import { generateAPIKeyName, apiKeyAsRuleDomainProperties } from '../../../../rules_client/common';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import { RulesClientContext, NormalizedAlertAction } from '../../../../rules_client/types';
|
||||
import { RulesClientContext } from '../../../../rules_client/types';
|
||||
import { RuleDomain, RuleParams } from '../../types';
|
||||
import { SanitizedRule } from '../../../../types';
|
||||
import {
|
||||
|
@ -55,14 +56,18 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
|
||||
): Promise<SanitizedRule<Params>> {
|
||||
const { data: initialData, options, allowMissingConnectorSecrets } = createParams;
|
||||
const actionsClient = await context.getActionsClient();
|
||||
|
||||
const { actions: genAction, systemActions: genSystemActions } = await addGeneratedActionValues(
|
||||
initialData.actions,
|
||||
initialData.systemActions,
|
||||
context
|
||||
);
|
||||
|
||||
// TODO (http-versioning): Remove this cast when we fix addGeneratedActionValues
|
||||
const data = {
|
||||
...initialData,
|
||||
actions: await addGeneratedActionValues(
|
||||
initialData.actions as NormalizedAlertAction[],
|
||||
context
|
||||
),
|
||||
actions: genAction,
|
||||
systemActions: genSystemActions,
|
||||
};
|
||||
|
||||
const id = options?.id || SavedObjectsUtils.generateId();
|
||||
|
@ -144,6 +149,14 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
validateActions(context, ruleType, data, allowMissingConnectorSecrets)
|
||||
);
|
||||
|
||||
await withSpan({ name: 'validateSystemActions', type: 'rules' }, () =>
|
||||
validateSystemActions({
|
||||
actionsClient,
|
||||
connectorAdapterRegistry: context.connectorAdapterRegistry,
|
||||
systemActions: data.systemActions,
|
||||
})
|
||||
);
|
||||
|
||||
// Throw error if schedule interval is less than the minimum and we are enforcing it
|
||||
const intervalInMs = parseDuration(data.schedule.interval);
|
||||
if (
|
||||
|
@ -155,13 +168,14 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
);
|
||||
}
|
||||
|
||||
const allActions = [...data.actions, ...(data.systemActions ?? [])];
|
||||
// Extract saved object references for this rule
|
||||
const {
|
||||
references,
|
||||
params: updatedParams,
|
||||
actions,
|
||||
actions: actionsWithRefs,
|
||||
} = await withSpan({ name: 'extractReferences', type: 'rules' }, () =>
|
||||
extractReferences(context, ruleType, data.actions, validatedRuleTypeParams)
|
||||
extractReferences(context, ruleType, allActions, validatedRuleTypeParams)
|
||||
);
|
||||
|
||||
const createTime = Date.now();
|
||||
|
@ -170,10 +184,12 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
|
||||
const throttle = data.throttle ?? null;
|
||||
|
||||
const { systemActions, actions: actionToNotUse, ...restData } = data;
|
||||
// Convert domain rule object to ES rule attributes
|
||||
const ruleAttributes = transformRuleDomainToRuleAttributes(
|
||||
{
|
||||
...data,
|
||||
const ruleAttributes = transformRuleDomainToRuleAttributes({
|
||||
actionsWithRefs,
|
||||
rule: {
|
||||
...restData,
|
||||
// TODO (http-versioning) create a rule domain version of this function
|
||||
// Right now this works because the 2 types can interop but it's not ideal
|
||||
...apiKeyAsRuleDomainProperties(createdAPIKey, username, isAuthTypeApiKey),
|
||||
|
@ -192,13 +208,12 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
revision: 0,
|
||||
running: false,
|
||||
},
|
||||
{
|
||||
params: {
|
||||
legacyId,
|
||||
actionsWithRefs: actions,
|
||||
// @ts-expect-error upgrade typescript v4.9.5
|
||||
paramsWithRefs: updatedParams,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const createdRuleSavedObject: SavedObject<RuleAttributes> = await withSpan(
|
||||
{ name: 'createRuleSavedObject', type: 'rules' },
|
||||
|
@ -221,7 +236,8 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
logger: context.logger,
|
||||
ruleType: context.ruleTypeRegistry.get(createdRuleSavedObject.attributes.alertTypeId),
|
||||
references,
|
||||
}
|
||||
},
|
||||
(connectorId: string) => actionsClient.isSystemAction(connectorId)
|
||||
);
|
||||
|
||||
// Try to validate created rule, but don't throw.
|
||||
|
|
|
@ -9,6 +9,30 @@ import { schema } from '@kbn/config-schema';
|
|||
import { validateDuration } from '../../../validation';
|
||||
import { notifyWhenSchema, actionAlertsFilterSchema, alertDelaySchema } from '../../../schemas';
|
||||
|
||||
export const defaultActionSchema = schema.object({
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
actionTypeId: schema.maybe(schema.string()),
|
||||
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
|
||||
frequency: schema.maybe(
|
||||
schema.object({
|
||||
summary: schema.boolean(),
|
||||
notifyWhen: notifyWhenSchema,
|
||||
throttle: schema.nullable(schema.string({ validate: validateDuration })),
|
||||
})
|
||||
),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
alertsFilter: schema.maybe(actionAlertsFilterSchema),
|
||||
useAlertDataForTemplate: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const systemActionSchema = schema.object({
|
||||
id: schema.string(),
|
||||
actionTypeId: schema.maybe(schema.string()),
|
||||
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export const createRuleDataSchema = schema.object({
|
||||
name: schema.string(),
|
||||
alertTypeId: schema.string(),
|
||||
|
@ -20,24 +44,13 @@ export const createRuleDataSchema = schema.object({
|
|||
schedule: schema.object({
|
||||
interval: schema.string({ validate: validateDuration }),
|
||||
}),
|
||||
actions: schema.arrayOf(
|
||||
schema.object({
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
actionTypeId: schema.maybe(schema.string()),
|
||||
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
|
||||
frequency: schema.maybe(
|
||||
schema.object({
|
||||
summary: schema.boolean(),
|
||||
notifyWhen: notifyWhenSchema,
|
||||
throttle: schema.nullable(schema.string({ validate: validateDuration })),
|
||||
})
|
||||
),
|
||||
uuid: schema.maybe(schema.string()),
|
||||
alertsFilter: schema.maybe(actionAlertsFilterSchema),
|
||||
useAlertDataForTemplate: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
{ defaultValue: [] }
|
||||
actions: schema.arrayOf(defaultActionSchema, {
|
||||
defaultValue: [],
|
||||
}),
|
||||
systemActions: schema.maybe(
|
||||
schema.arrayOf(systemActionSchema, {
|
||||
defaultValue: [],
|
||||
})
|
||||
),
|
||||
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
|
||||
alertDelay: schema.maybe(alertDelaySchema),
|
||||
|
|
|
@ -5,4 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { createRuleDataSchema } from './create_rule_data_schema';
|
||||
export {
|
||||
createRuleDataSchema,
|
||||
defaultActionSchema,
|
||||
systemActionSchema,
|
||||
} from './create_rule_data_schema';
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { createRuleDataSchema } from '../schemas';
|
||||
import { createRuleDataSchema, defaultActionSchema, systemActionSchema } from '../schemas';
|
||||
import { RuleParams } from '../../../types';
|
||||
|
||||
type CreateRuleDataType = TypeOf<typeof createRuleDataSchema>;
|
||||
type CreateRuleActionDataType = TypeOf<typeof defaultActionSchema>;
|
||||
type CreateRuleSystemActionDataType = TypeOf<typeof systemActionSchema>;
|
||||
|
||||
export interface CreateRuleData<Params extends RuleParams = never> {
|
||||
name: CreateRuleDataType['name'];
|
||||
|
@ -20,7 +22,8 @@ export interface CreateRuleData<Params extends RuleParams = never> {
|
|||
throttle?: CreateRuleDataType['throttle'];
|
||||
params: Params;
|
||||
schedule: CreateRuleDataType['schedule'];
|
||||
actions: CreateRuleDataType['actions'];
|
||||
actions: CreateRuleActionDataType[];
|
||||
systemActions?: CreateRuleSystemActionDataType[];
|
||||
notifyWhen?: CreateRuleDataType['notifyWhen'];
|
||||
alertDelay?: CreateRuleDataType['alertDelay'];
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ 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';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
@ -54,9 +55,11 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
isSystemAction: jest.fn(),
|
||||
};
|
||||
|
||||
const getMockAggregationResult = (
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { ConstructorOptions, RulesClient } 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 { 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';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
import { getBeforeSetup } from '../../../../rules_client/tests/lib';
|
||||
|
||||
describe('resolve', () => {
|
||||
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.2.0';
|
||||
const createAPIKeyMock = jest.fn();
|
||||
const isAuthenticationTypeApiKeyMock = jest.fn();
|
||||
const getAuthenticationApiKeyMock = jest.fn();
|
||||
|
||||
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: 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,
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
isSystemAction: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
let rulesClient: RulesClient;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
|
||||
rulesClient = new RulesClient(rulesClientParams);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('transform actions correctly', async () => {
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValue({
|
||||
outcome: 'exactMatch',
|
||||
saved_object: {
|
||||
id: 'test-rule',
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
frequency: {
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
params: {},
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test-1',
|
||||
uuid: '222',
|
||||
},
|
||||
{
|
||||
params: {},
|
||||
actionRef: 'system_action:system_action-id',
|
||||
actionTypeId: 'test-2',
|
||||
uuid: '222',
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
executionStatus: {},
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const res = await rulesClient.resolve({ id: 'test-rule' });
|
||||
|
||||
expect(res.actions).toEqual([
|
||||
{
|
||||
actionTypeId: 'test-1',
|
||||
frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null },
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {},
|
||||
uuid: '222',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(res.systemActions).toEqual([
|
||||
{ actionTypeId: 'test-2', id: 'system_action-id', params: {}, uuid: '222' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -66,13 +66,17 @@ Promise<ResolvedSanitizedRule<Params>> {
|
|||
})
|
||||
);
|
||||
|
||||
const ruleDomain = transformRuleAttributesToRuleDomain(result.attributes, {
|
||||
id: result.id,
|
||||
logger: context.logger,
|
||||
ruleType: context.ruleTypeRegistry.get(result.attributes.alertTypeId),
|
||||
references: result.references,
|
||||
includeSnoozeData,
|
||||
});
|
||||
const ruleDomain = transformRuleAttributesToRuleDomain(
|
||||
result.attributes,
|
||||
{
|
||||
id: result.id,
|
||||
logger: context.logger,
|
||||
ruleType: context.ruleTypeRegistry.get(result.attributes.alertTypeId),
|
||||
references: result.references,
|
||||
includeSnoozeData,
|
||||
},
|
||||
context.isSystemAction
|
||||
);
|
||||
|
||||
const rule = transformRuleDomainToRule(ruleDomain);
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
|||
import { getBeforeSetup } from '../../../../rules_client/tests/lib';
|
||||
import { RecoveredActionGroup } from '../../../../../common';
|
||||
import { RegistryRuleType } from '../../../../rule_type_registry';
|
||||
import { ConnectorAdapterRegistry } from '../../../../connector_adapters/connector_adapter_registry';
|
||||
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
|
@ -56,9 +57,11 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
kibanaVersion,
|
||||
isAuthenticationTypeAPIKey: jest.fn(),
|
||||
getAuthenticationAPIKey: jest.fn(),
|
||||
connectorAdapterRegistry: new ConnectorAdapterRegistry(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
alertsService: null,
|
||||
uiSettings: uiSettingsServiceMock.createStartContract(),
|
||||
isSystemAction: jest.fn(),
|
||||
};
|
||||
|
||||
const listedTypes = new Set<RegistryRuleType>([
|
||||
|
|
|
@ -44,7 +44,7 @@ const actionFrequencySchema = schema.object({
|
|||
/**
|
||||
* Unsanitized (domain) action schema, used by internal rules clients
|
||||
*/
|
||||
export const actionDomainSchema = schema.object({
|
||||
export const defaultActionDomainSchema = schema.object({
|
||||
uuid: schema.maybe(schema.string()),
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
|
@ -52,7 +52,14 @@ export const actionDomainSchema = schema.object({
|
|||
params: actionParamsSchema,
|
||||
frequency: schema.maybe(actionFrequencySchema),
|
||||
alertsFilter: schema.maybe(actionDomainAlertsFilterSchema),
|
||||
useAlertDataAsTemplate: schema.maybe(schema.boolean()),
|
||||
useAlertDataForTemplate: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const systemActionDomainSchema = schema.object({
|
||||
id: schema.string(),
|
||||
actionTypeId: schema.string(),
|
||||
params: actionParamsSchema,
|
||||
uuid: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export const actionAlertsFilterSchema = schema.object({
|
||||
|
@ -60,7 +67,7 @@ export const actionAlertsFilterSchema = schema.object({
|
|||
timeframe: schema.maybe(actionAlertsFilterTimeFrameSchema),
|
||||
});
|
||||
|
||||
export const actionSchema = schema.object({
|
||||
export const defaultActionSchema = schema.object({
|
||||
uuid: schema.maybe(schema.string()),
|
||||
group: schema.string(),
|
||||
id: schema.string(),
|
||||
|
@ -70,3 +77,11 @@ export const actionSchema = schema.object({
|
|||
alertsFilter: schema.maybe(actionAlertsFilterSchema),
|
||||
useAlertDataForTemplate: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const systemActionSchema = schema.object({
|
||||
id: schema.string(),
|
||||
actionTypeId: schema.string(),
|
||||
params: actionParamsSchema,
|
||||
uuid: schema.maybe(schema.string()),
|
||||
useAlertDataAsTemplate: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
|
|
@ -18,8 +18,8 @@ export {
|
|||
|
||||
export {
|
||||
actionParamsSchema,
|
||||
actionDomainSchema,
|
||||
actionSchema,
|
||||
defaultActionDomainSchema,
|
||||
systemActionDomainSchema,
|
||||
actionAlertsFilterSchema,
|
||||
} from './action_schemas';
|
||||
|
||||
|
|
|
@ -15,7 +15,12 @@ import {
|
|||
import { rRuleSchema } from '../../r_rule/schemas';
|
||||
import { dateSchema } from './date_schema';
|
||||
import { notifyWhenSchema } from './notify_when_schema';
|
||||
import { actionDomainSchema, actionSchema } from './action_schemas';
|
||||
import {
|
||||
defaultActionDomainSchema,
|
||||
defaultActionSchema,
|
||||
systemActionDomainSchema,
|
||||
systemActionSchema,
|
||||
} from './action_schemas';
|
||||
|
||||
export const ruleParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
|
||||
export const mappedParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
|
||||
|
@ -147,7 +152,8 @@ export const ruleDomainSchema = schema.object({
|
|||
alertTypeId: schema.string(),
|
||||
consumer: schema.string(),
|
||||
schedule: intervalScheduleSchema,
|
||||
actions: schema.arrayOf(actionDomainSchema),
|
||||
actions: schema.arrayOf(defaultActionDomainSchema),
|
||||
systemActions: schema.maybe(schema.arrayOf(systemActionDomainSchema)),
|
||||
params: ruleParamsSchema,
|
||||
mapped_params: schema.maybe(mappedParamsSchema),
|
||||
scheduledTaskId: schema.maybe(schema.string()),
|
||||
|
@ -186,7 +192,8 @@ export const ruleSchema = schema.object({
|
|||
alertTypeId: schema.string(),
|
||||
consumer: schema.string(),
|
||||
schedule: intervalScheduleSchema,
|
||||
actions: schema.arrayOf(actionSchema),
|
||||
actions: schema.arrayOf(defaultActionSchema),
|
||||
systemActions: schema.maybe(schema.arrayOf(systemActionSchema)),
|
||||
params: ruleParamsSchema,
|
||||
mapped_params: schema.maybe(mappedParamsSchema),
|
||||
scheduledTaskId: schema.maybe(schema.string()),
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
export { transformRuleAttributesToRuleDomain } from './transform_rule_attributes_to_rule_domain';
|
||||
export { transformRuleDomainToRuleAttributes } from './transform_rule_domain_to_rule_attributes';
|
||||
export { transformRuleDomainToRule } from './transform_rule_domain_to_rule';
|
||||
export { transformRawActionsToDomainActions } from './transform_raw_actions_to_domain_actions';
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { RuleActionAttributes } from '../../../data/rule/types';
|
||||
import {
|
||||
transformRawActionsToDomainActions,
|
||||
transformRawActionsToDomainSystemActions,
|
||||
} from './transform_raw_actions_to_domain_actions';
|
||||
|
||||
const defaultAction: RuleActionAttributes = {
|
||||
group: 'default',
|
||||
uuid: '1',
|
||||
actionRef: 'default-action-ref',
|
||||
actionTypeId: '.test',
|
||||
params: {},
|
||||
frequency: {
|
||||
summary: false,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
throttle: '1m',
|
||||
},
|
||||
alertsFilter: { query: { kql: 'test:1', dsl: '{}', filters: [] } },
|
||||
};
|
||||
|
||||
const systemAction: RuleActionAttributes = {
|
||||
actionRef: 'system_action:my-system-action-id',
|
||||
uuid: '123',
|
||||
actionTypeId: '.test-system-action',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const isSystemAction = (id: string) => id === 'my-system-action-id';
|
||||
|
||||
describe('transformRawActionsToDomainActions', () => {
|
||||
it('transforms the actions correctly', () => {
|
||||
const res = transformRawActionsToDomainActions({
|
||||
actions: [defaultAction, systemAction],
|
||||
ruleId: 'test-rule',
|
||||
references: [{ name: 'default-action-ref', id: 'default-action-id', type: 'action' }],
|
||||
isSystemAction,
|
||||
});
|
||||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": ".test",
|
||||
"alertsFilter": Object {
|
||||
"query": Object {
|
||||
"filters": Array [],
|
||||
"kql": "test:1",
|
||||
},
|
||||
},
|
||||
"frequency": Object {
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"summary": false,
|
||||
"throttle": "1m",
|
||||
},
|
||||
"group": "default",
|
||||
"id": "default-action-id",
|
||||
"params": Object {},
|
||||
"uuid": "1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformRawActionsToDomainSystemActions', () => {
|
||||
it('transforms the system actions correctly', () => {
|
||||
const res = transformRawActionsToDomainSystemActions({
|
||||
actions: [defaultAction, systemAction],
|
||||
ruleId: 'test-rule',
|
||||
references: [{ name: 'default-action-ref', id: 'default-action-id', type: 'action' }],
|
||||
isSystemAction,
|
||||
});
|
||||
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": ".test-system-action",
|
||||
"id": "my-system-action-id",
|
||||
"params": Object {},
|
||||
"uuid": "123",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { omit } from 'lodash';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import { injectReferencesIntoActions } from '../../../rules_client/common';
|
||||
import { RuleAttributes } from '../../../data/rule/types';
|
||||
import { RawRule } from '../../../types';
|
||||
import { RuleDomain } from '../types';
|
||||
|
||||
interface Args {
|
||||
ruleId: string;
|
||||
actions: RuleAttributes['actions'] | RawRule['actions'];
|
||||
isSystemAction: (connectorId: string) => boolean;
|
||||
omitGeneratedValues?: boolean;
|
||||
references?: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export const transformRawActionsToDomainActions = ({
|
||||
actions,
|
||||
ruleId,
|
||||
references,
|
||||
omitGeneratedValues = true,
|
||||
isSystemAction,
|
||||
}: Args): RuleDomain['actions'] => {
|
||||
const actionsWithInjectedRefs = actions
|
||||
? injectReferencesIntoActions(ruleId, actions, references || [])
|
||||
: [];
|
||||
|
||||
const ruleDomainActions = actionsWithInjectedRefs
|
||||
.filter((action) => !isSystemAction(action.id))
|
||||
.map((action) => {
|
||||
const defaultAction = {
|
||||
group: action.group ?? 'default',
|
||||
id: action.id,
|
||||
params: action.params,
|
||||
actionTypeId: action.actionTypeId,
|
||||
uuid: action.uuid,
|
||||
...(action.frequency ? { frequency: action.frequency } : {}),
|
||||
...(action.alertsFilter ? { alertsFilter: action.alertsFilter } : {}),
|
||||
...(action.useAlertDataAsTemplate
|
||||
? { useAlertDataAsTemplate: action.useAlertDataAsTemplate }
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (omitGeneratedValues) {
|
||||
return omit(defaultAction, 'alertsFilter.query.dsl');
|
||||
}
|
||||
|
||||
return defaultAction;
|
||||
});
|
||||
|
||||
return ruleDomainActions;
|
||||
};
|
||||
|
||||
export const transformRawActionsToDomainSystemActions = ({
|
||||
actions,
|
||||
ruleId,
|
||||
references,
|
||||
omitGeneratedValues = true,
|
||||
isSystemAction,
|
||||
}: Args): RuleDomain['systemActions'] => {
|
||||
const actionsWithInjectedRefs = actions
|
||||
? injectReferencesIntoActions(ruleId, actions, references || [])
|
||||
: [];
|
||||
|
||||
const ruleDomainSystemActions = actionsWithInjectedRefs
|
||||
.filter((action) => isSystemAction(action.id))
|
||||
.map((action) => {
|
||||
return {
|
||||
id: action.id,
|
||||
params: action.params,
|
||||
actionTypeId: action.actionTypeId,
|
||||
uuid: action.uuid,
|
||||
...(action.useAlertDataAsTemplate
|
||||
? { useAlertDataAsTemplate: action.useAlertDataAsTemplate }
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
|
||||
return ruleDomainSystemActions;
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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 { RecoveredActionGroup } from '../../../../common';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { transformRuleAttributesToRuleDomain } from './transform_rule_attributes_to_rule_domain';
|
||||
import { UntypedNormalizedRuleType } from '../../../rule_type_registry';
|
||||
import { RuleActionAttributes } from '../../../data/rule/types';
|
||||
|
||||
const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
||||
id: 'test.rule-type',
|
||||
name: 'My test rule',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
autoRecoverAlerts: true,
|
||||
doesSetRecoveryContext: true,
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
},
|
||||
alerts: {
|
||||
context: 'test',
|
||||
mappings: { fieldMap: { field: { type: 'keyword', required: false } } },
|
||||
shouldWrite: true,
|
||||
},
|
||||
category: 'test',
|
||||
validLegacyConsumers: [],
|
||||
};
|
||||
|
||||
const defaultAction: RuleActionAttributes = {
|
||||
group: 'default',
|
||||
uuid: '1',
|
||||
actionRef: 'default-action-ref',
|
||||
actionTypeId: '.test',
|
||||
params: {},
|
||||
frequency: {
|
||||
summary: false,
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
throttle: '1m',
|
||||
},
|
||||
alertsFilter: { query: { kql: 'test:1', dsl: '{}', filters: [] } },
|
||||
};
|
||||
|
||||
const systemAction: RuleActionAttributes = {
|
||||
actionRef: 'system_action:my-system-action-id',
|
||||
uuid: '123',
|
||||
actionTypeId: '.test-system-action',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const isSystemAction = (id: string) => id === 'my-system-action-id';
|
||||
|
||||
describe('transformRuleAttributesToRuleDomain', () => {
|
||||
const MOCK_API_KEY = Buffer.from('123:abc').toString('base64');
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const references = [{ name: 'default-action-ref', type: 'action', id: 'default-action-id' }];
|
||||
|
||||
const rule = {
|
||||
enabled: false,
|
||||
tags: ['foo'],
|
||||
createdBy: 'user',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
legacyId: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
snoozeSchedule: [],
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '1m' },
|
||||
consumer: 'myApp',
|
||||
scheduledTaskId: 'task-123',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
status: 'pending' as const,
|
||||
},
|
||||
params: {},
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
actions: [defaultAction, systemAction],
|
||||
name: 'my rule name',
|
||||
revision: 0,
|
||||
updatedBy: 'user',
|
||||
apiKey: MOCK_API_KEY,
|
||||
apiKeyOwner: 'user',
|
||||
};
|
||||
|
||||
it('transforms the actions correctly', () => {
|
||||
const res = transformRuleAttributesToRuleDomain(
|
||||
rule,
|
||||
{
|
||||
id: '1',
|
||||
logger,
|
||||
ruleType,
|
||||
references,
|
||||
},
|
||||
isSystemAction
|
||||
);
|
||||
|
||||
expect(res.actions).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": ".test",
|
||||
"alertsFilter": Object {
|
||||
"query": Object {
|
||||
"filters": Array [],
|
||||
"kql": "test:1",
|
||||
},
|
||||
},
|
||||
"frequency": Object {
|
||||
"notifyWhen": "onThrottleInterval",
|
||||
"summary": false,
|
||||
"throttle": "1m",
|
||||
},
|
||||
"group": "default",
|
||||
"id": "default-action-id",
|
||||
"params": Object {},
|
||||
"uuid": "1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(res.systemActions).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actionTypeId": ".test-system-action",
|
||||
"id": "my-system-action-id",
|
||||
"params": Object {},
|
||||
"uuid": "123",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -4,20 +4,21 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { omit, isEmpty } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { SavedObjectReference } from '@kbn/core/server';
|
||||
import { ruleExecutionStatusValues } from '../constants';
|
||||
import { getRuleSnoozeEndTime } from '../../../lib';
|
||||
import { RuleDomain, Monitoring, RuleParams } from '../types';
|
||||
import { RuleAttributes } from '../../../data/rule/types';
|
||||
import { RawRule, PartialRule } from '../../../types';
|
||||
import { PartialRule } from '../../../types';
|
||||
import { UntypedNormalizedRuleType } from '../../../rule_type_registry';
|
||||
import {
|
||||
injectReferencesIntoActions,
|
||||
injectReferencesIntoParams,
|
||||
} from '../../../rules_client/common';
|
||||
import { injectReferencesIntoParams } from '../../../rules_client/common';
|
||||
import { getActiveScheduledSnoozes } from '../../../lib/is_rule_snoozed';
|
||||
import {
|
||||
transformRawActionsToDomainActions,
|
||||
transformRawActionsToDomainSystemActions,
|
||||
} from './transform_raw_actions_to_domain_actions';
|
||||
|
||||
const INITIAL_LAST_RUN_METRICS = {
|
||||
duration: 0,
|
||||
|
@ -120,7 +121,8 @@ interface TransformEsToRuleParams {
|
|||
|
||||
export const transformRuleAttributesToRuleDomain = <Params extends RuleParams = never>(
|
||||
esRule: RuleAttributes,
|
||||
transformParams: TransformEsToRuleParams
|
||||
transformParams: TransformEsToRuleParams,
|
||||
isSystemAction: (connectorId: string) => boolean
|
||||
): RuleDomain<Params> => {
|
||||
const { scheduledTaskId, executionStatus, monitoring, snoozeSchedule, lastRun } = esRule;
|
||||
|
||||
|
@ -141,6 +143,7 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
|
|||
...(s.rRule.until ? { until: new Date(s.rRule.until).toISOString() } : {}),
|
||||
},
|
||||
}));
|
||||
|
||||
const includeSnoozeSchedule = snoozeSchedule !== undefined && !isEmpty(snoozeSchedule);
|
||||
const isSnoozedUntil = includeSnoozeSchedule
|
||||
? getRuleSnoozeEndTime({
|
||||
|
@ -149,13 +152,21 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
|
|||
})?.toISOString()
|
||||
: null;
|
||||
|
||||
let actions = esRule.actions
|
||||
? injectReferencesIntoActions(id, esRule.actions as RawRule['actions'], references || [])
|
||||
: [];
|
||||
|
||||
if (omitGeneratedValues) {
|
||||
actions = actions.map((ruleAction) => omit(ruleAction, 'alertsFilter.query.dsl'));
|
||||
}
|
||||
const ruleDomainActions: RuleDomain['actions'] = transformRawActionsToDomainActions({
|
||||
ruleId: id,
|
||||
actions: esRule.actions,
|
||||
references,
|
||||
isSystemAction,
|
||||
omitGeneratedValues,
|
||||
});
|
||||
const ruleDomainSystemActions: RuleDomain['systemActions'] =
|
||||
transformRawActionsToDomainSystemActions({
|
||||
ruleId: id,
|
||||
actions: esRule.actions,
|
||||
references,
|
||||
isSystemAction,
|
||||
omitGeneratedValues,
|
||||
});
|
||||
|
||||
const params = injectReferencesIntoParams<Params, RuleParams>(
|
||||
id,
|
||||
|
@ -177,7 +188,8 @@ export const transformRuleAttributesToRuleDomain = <Params extends RuleParams =
|
|||
alertTypeId: esRule.alertTypeId,
|
||||
consumer: esRule.consumer,
|
||||
schedule: esRule.schedule,
|
||||
actions: actions as RuleDomain['actions'],
|
||||
actions: ruleDomainActions,
|
||||
systemActions: ruleDomainSystemActions,
|
||||
params,
|
||||
mapped_params: esRule.mapped_params,
|
||||
...(scheduledTaskId ? { scheduledTaskId } : {}),
|
||||
|
|
|
@ -26,6 +26,7 @@ export const transformRuleDomainToRule = <Params extends RuleParams = never>(
|
|||
consumer: ruleDomain.consumer,
|
||||
schedule: ruleDomain.schedule,
|
||||
actions: ruleDomain.actions,
|
||||
systemActions: ruleDomain.systemActions,
|
||||
params: ruleDomain.params,
|
||||
mapped_params: ruleDomain.mapped_params,
|
||||
scheduledTaskId: ruleDomain.scheduledTaskId,
|
||||
|
|
|
@ -7,19 +7,24 @@
|
|||
import { RuleDomain } from '../types';
|
||||
import { RuleAttributes } from '../../../data/rule/types';
|
||||
import { getMappedParams } from '../../../rules_client/common';
|
||||
import { DenormalizedAction } from '../../../rules_client';
|
||||
|
||||
interface TransformRuleToEsParams {
|
||||
legacyId: RuleAttributes['legacyId'];
|
||||
actionsWithRefs: RuleAttributes['actions'];
|
||||
paramsWithRefs: RuleAttributes['params'];
|
||||
meta?: RuleAttributes['meta'];
|
||||
}
|
||||
|
||||
export const transformRuleDomainToRuleAttributes = (
|
||||
rule: Omit<RuleDomain, 'actions' | 'params'>,
|
||||
params: TransformRuleToEsParams
|
||||
): RuleAttributes => {
|
||||
const { legacyId, actionsWithRefs, paramsWithRefs, meta } = params;
|
||||
export const transformRuleDomainToRuleAttributes = ({
|
||||
actionsWithRefs,
|
||||
rule,
|
||||
params,
|
||||
}: {
|
||||
actionsWithRefs: DenormalizedAction[];
|
||||
rule: Omit<RuleDomain, 'actions' | 'params' | 'systemActions'>;
|
||||
params: TransformRuleToEsParams;
|
||||
}): RuleAttributes => {
|
||||
const { legacyId, paramsWithRefs, meta } = params;
|
||||
const mappedParams = getMappedParams(paramsWithRefs);
|
||||
|
||||
return {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
ruleExecutionStatusValues,
|
||||
ruleExecutionStatusErrorReason,
|
||||
ruleExecutionStatusWarningReason,
|
||||
filterStateStore,
|
||||
} from '../constants';
|
||||
import {
|
||||
ruleParamsSchema,
|
||||
|
@ -20,7 +19,6 @@ import {
|
|||
ruleExecutionStatusSchema,
|
||||
ruleLastRunSchema,
|
||||
monitoringSchema,
|
||||
actionSchema,
|
||||
ruleSchema,
|
||||
ruleDomainSchema,
|
||||
} from '../schemas';
|
||||
|
@ -34,13 +32,11 @@ export type RuleExecutionStatusErrorReason =
|
|||
typeof ruleExecutionStatusErrorReason[keyof typeof ruleExecutionStatusErrorReason];
|
||||
export type RuleExecutionStatusWarningReason =
|
||||
typeof ruleExecutionStatusWarningReason[keyof typeof ruleExecutionStatusWarningReason];
|
||||
export type FilterStateStore = typeof filterStateStore[keyof typeof filterStateStore];
|
||||
|
||||
export type RuleParams = TypeOf<typeof ruleParamsSchema>;
|
||||
export type RuleSnoozeSchedule = TypeOf<typeof snoozeScheduleSchema>;
|
||||
export type RuleLastRun = TypeOf<typeof ruleLastRunSchema>;
|
||||
export type Monitoring = TypeOf<typeof monitoringSchema>;
|
||||
export type Action = TypeOf<typeof actionSchema>;
|
||||
type RuleSchemaType = TypeOf<typeof ruleSchema>;
|
||||
type RuleDomainSchemaType = TypeOf<typeof ruleDomainSchema>;
|
||||
|
||||
|
@ -62,6 +58,7 @@ export interface Rule<Params extends RuleParams = never> {
|
|||
consumer: RuleSchemaType['consumer'];
|
||||
schedule: RuleSchemaType['schedule'];
|
||||
actions: RuleSchemaType['actions'];
|
||||
systemActions?: RuleSchemaType['systemActions'];
|
||||
params: Params;
|
||||
mapped_params?: RuleSchemaType['mapped_params'];
|
||||
scheduledTaskId?: RuleSchemaType['scheduledTaskId'];
|
||||
|
@ -97,6 +94,7 @@ export interface RuleDomain<Params extends RuleParams = never> {
|
|||
consumer: RuleDomainSchemaType['consumer'];
|
||||
schedule: RuleDomainSchemaType['schedule'];
|
||||
actions: RuleDomainSchemaType['actions'];
|
||||
systemActions: RuleDomainSchemaType['systemActions'];
|
||||
params: Params;
|
||||
mapped_params?: RuleDomainSchemaType['mapped_params'];
|
||||
scheduledTaskId?: RuleDomainSchemaType['scheduledTaskId'];
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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';
|
||||
import { ConnectorAdapterRegistry } from './connector_adapter_registry';
|
||||
import type { ConnectorAdapter } from './types';
|
||||
|
||||
describe('ConnectorAdapterRegistry', () => {
|
||||
const connectorAdapter: ConnectorAdapter = {
|
||||
connectorTypeId: '.test',
|
||||
ruleActionParamsSchema: schema.object({}),
|
||||
buildActionParams: jest.fn(),
|
||||
};
|
||||
|
||||
let registry: ConnectorAdapterRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ConnectorAdapterRegistry();
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('returns true if the connector adapter is registered', () => {
|
||||
registry.register(connectorAdapter);
|
||||
expect(registry.has('.test')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the connector adapter is not registered', () => {
|
||||
expect(registry.has('.not-exist')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('registers a connector adapter correctly', () => {
|
||||
registry.register(connectorAdapter);
|
||||
expect(registry.get('.test')).toEqual(connectorAdapter);
|
||||
});
|
||||
|
||||
it('throws an error if the connector adapter exists', () => {
|
||||
registry.register(connectorAdapter);
|
||||
|
||||
expect(() => registry.register(connectorAdapter)).toThrowErrorMatchingInlineSnapshot(
|
||||
`".test is already registered to the ConnectorAdapterRegistry"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('gets a connector adapter correctly', () => {
|
||||
registry.register(connectorAdapter);
|
||||
expect(registry.get('.test')).toEqual(connectorAdapter);
|
||||
});
|
||||
|
||||
it('throws an error if the connector adapter does not exists', () => {
|
||||
expect(() => registry.get('.not-exists')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Connector adapter \\".not-exists\\" is not registered."`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ConnectorAdapter, ConnectorAdapterParams } from './types';
|
||||
|
||||
export class ConnectorAdapterRegistry {
|
||||
private readonly connectorAdapters: Map<string, ConnectorAdapter> = new Map();
|
||||
|
||||
public has(connectorTypeId: string): boolean {
|
||||
return this.connectorAdapters.has(connectorTypeId);
|
||||
}
|
||||
|
||||
public register<
|
||||
RuleActionParams extends ConnectorAdapterParams = ConnectorAdapterParams,
|
||||
ConnectorParams extends ConnectorAdapterParams = ConnectorAdapterParams
|
||||
>(connectorAdapter: ConnectorAdapter<RuleActionParams, ConnectorParams>) {
|
||||
if (this.has(connectorAdapter.connectorTypeId)) {
|
||||
throw new Error(
|
||||
`${connectorAdapter.connectorTypeId} is already registered to the ConnectorAdapterRegistry`
|
||||
);
|
||||
}
|
||||
|
||||
this.connectorAdapters.set(
|
||||
connectorAdapter.connectorTypeId,
|
||||
connectorAdapter as unknown as ConnectorAdapter
|
||||
);
|
||||
}
|
||||
|
||||
public get(connectorTypeId: string): ConnectorAdapter {
|
||||
if (!this.connectorAdapters.has(connectorTypeId)) {
|
||||
throw Boom.badRequest(
|
||||
i18n.translate(
|
||||
'xpack.alerting.connectorAdapterRegistry.get.missingConnectorAdapterErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Connector adapter "{connectorTypeId}" is not registered.',
|
||||
values: {
|
||||
connectorTypeId,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return this.connectorAdapters.get(connectorTypeId)!;
|
||||
}
|
||||
}
|
41
x-pack/plugins/alerting/server/connector_adapters/types.ts
Normal file
41
x-pack/plugins/alerting/server/connector_adapters/types.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { ObjectType } from '@kbn/config-schema';
|
||||
import type { RuleTypeParams, SanitizedRule } from '../../common';
|
||||
import { CombinedSummarizedAlerts } from '../types';
|
||||
|
||||
type Rule = Pick<SanitizedRule<RuleTypeParams>, 'id' | 'name' | 'tags'>;
|
||||
|
||||
export interface ConnectorAdapterParams {
|
||||
[x: string]: unknown;
|
||||
}
|
||||
|
||||
interface BuildActionParamsArgs<RuleActionParams> {
|
||||
alerts: CombinedSummarizedAlerts;
|
||||
rule: Rule;
|
||||
params: RuleActionParams;
|
||||
spaceId: string;
|
||||
ruleUrl?: string;
|
||||
}
|
||||
|
||||
export interface ConnectorAdapter<
|
||||
RuleActionParams extends ConnectorAdapterParams = ConnectorAdapterParams,
|
||||
ConnectorParams extends ConnectorAdapterParams = ConnectorAdapterParams
|
||||
> {
|
||||
connectorTypeId: string;
|
||||
/**
|
||||
* The schema of the action persisted
|
||||
* in the rule. The schema will be validated
|
||||
* when a rule is created or updated.
|
||||
* The schema should be backwards compatible
|
||||
* and should never introduce any breaking
|
||||
* changes.
|
||||
*/
|
||||
ruleActionParamsSchema: ObjectType;
|
||||
buildActionParams: (args: BuildActionParamsArgs<RuleActionParams>) => ConnectorParams;
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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';
|
||||
import { ConnectorAdapterRegistry } from './connector_adapter_registry';
|
||||
import type { ConnectorAdapter } from './types';
|
||||
import {
|
||||
bulkValidateConnectorAdapterActionParams,
|
||||
validateConnectorAdapterActionParams,
|
||||
} from './validate_rule_action_params';
|
||||
|
||||
describe('validateRuleActionParams', () => {
|
||||
const firstConnectorAdapter: ConnectorAdapter = {
|
||||
connectorTypeId: '.test',
|
||||
ruleActionParamsSchema: schema.object({ foo: schema.string() }),
|
||||
buildActionParams: jest.fn(),
|
||||
};
|
||||
|
||||
const secondConnectorAdapter: ConnectorAdapter = {
|
||||
connectorTypeId: '.test-2',
|
||||
ruleActionParamsSchema: schema.object({ bar: schema.string() }),
|
||||
buildActionParams: jest.fn(),
|
||||
};
|
||||
|
||||
let registry: ConnectorAdapterRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ConnectorAdapterRegistry();
|
||||
});
|
||||
|
||||
describe('validateConnectorAdapterActionParams', () => {
|
||||
it('should validate correctly invalid params', () => {
|
||||
registry.register(firstConnectorAdapter);
|
||||
|
||||
expect(() =>
|
||||
validateConnectorAdapterActionParams({
|
||||
connectorAdapterRegistry: registry,
|
||||
connectorTypeId: firstConnectorAdapter.connectorTypeId,
|
||||
params: { foo: 5 },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid system action params. System action type: .test - [foo]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw if the connectorTypeId is not defined', () => {
|
||||
registry.register(firstConnectorAdapter);
|
||||
|
||||
expect(() =>
|
||||
validateConnectorAdapterActionParams({
|
||||
connectorAdapterRegistry: registry,
|
||||
params: {},
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw if the connector adapter is not registered', () => {
|
||||
expect(() =>
|
||||
validateConnectorAdapterActionParams({
|
||||
connectorAdapterRegistry: registry,
|
||||
connectorTypeId: firstConnectorAdapter.connectorTypeId,
|
||||
params: {},
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkValidateConnectorAdapterActionParams', () => {
|
||||
it('should validate correctly invalid params with multiple actions', () => {
|
||||
const actions = [
|
||||
{ actionTypeId: firstConnectorAdapter.connectorTypeId, params: { foo: 5 } },
|
||||
{ actionTypeId: secondConnectorAdapter.connectorTypeId, params: { bar: 'test' } },
|
||||
];
|
||||
|
||||
registry.register(firstConnectorAdapter);
|
||||
registry.register(secondConnectorAdapter);
|
||||
|
||||
expect(() =>
|
||||
bulkValidateConnectorAdapterActionParams({
|
||||
connectorAdapterRegistry: registry,
|
||||
actions,
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid system action params. System action type: .test - [foo]: expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { ConnectorAdapterRegistry } from './connector_adapter_registry';
|
||||
|
||||
interface ValidateSchemaArgs {
|
||||
connectorAdapterRegistry: ConnectorAdapterRegistry;
|
||||
connectorTypeId?: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface BulkValidateSchemaArgs {
|
||||
connectorAdapterRegistry: ConnectorAdapterRegistry;
|
||||
actions: Array<{ actionTypeId: string; params: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
export const validateConnectorAdapterActionParams = ({
|
||||
connectorAdapterRegistry,
|
||||
connectorTypeId,
|
||||
params,
|
||||
}: ValidateSchemaArgs) => {
|
||||
if (!connectorTypeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connectorAdapterRegistry.has(connectorTypeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectorAdapter = connectorAdapterRegistry.get(connectorTypeId);
|
||||
const schema = connectorAdapter.ruleActionParamsSchema;
|
||||
|
||||
try {
|
||||
schema.validate(params);
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(
|
||||
`Invalid system action params. System action type: ${connectorAdapter.connectorTypeId} - ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const bulkValidateConnectorAdapterActionParams = ({
|
||||
connectorAdapterRegistry,
|
||||
actions,
|
||||
}: BulkValidateSchemaArgs) => {
|
||||
for (const action of actions) {
|
||||
validateConnectorAdapterActionParams({
|
||||
connectorAdapterRegistry,
|
||||
connectorTypeId: action.actionTypeId,
|
||||
params: action.params,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FilterStateStore } from '../constants';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
export interface AlertsFilterAttributes {
|
||||
query?: Record<string, unknown>;
|
||||
|
@ -18,5 +18,5 @@ export interface AlertsFilterAttributes {
|
|||
export interface AlertsFilterQueryAttributes {
|
||||
kql: string;
|
||||
filters: AlertsFilterAttributes[];
|
||||
dsl?: string;
|
||||
dsl: string;
|
||||
}
|
||||
|
|
|
@ -114,14 +114,14 @@ interface AlertsFilterTimeFrameAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
interface AlertsFilterAttributes {
|
||||
export interface AlertsFilterAttributes {
|
||||
query?: AlertsFilterQueryAttributes;
|
||||
timeframe?: AlertsFilterTimeFrameAttributes;
|
||||
}
|
||||
|
||||
export interface RuleActionAttributes {
|
||||
uuid: string;
|
||||
group: string;
|
||||
group?: string;
|
||||
actionRef: string;
|
||||
actionTypeId: string;
|
||||
params: SavedObjectAttributes;
|
||||
|
@ -131,6 +131,7 @@ export interface RuleActionAttributes {
|
|||
throttle: string | null;
|
||||
};
|
||||
alertsFilter?: AlertsFilterAttributes;
|
||||
useAlertDataAsTemplate?: boolean;
|
||||
}
|
||||
|
||||
type MappedParamsAttributes = SavedObjectAttributes & {
|
||||
|
|
|
@ -72,6 +72,7 @@ export {
|
|||
} from './alerts_service';
|
||||
export { sanitizeBulkErrorResponse, AlertsClientError } from './alerts_client';
|
||||
export { getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter';
|
||||
export type { ConnectorAdapter } from './connector_adapters/types';
|
||||
|
||||
export const plugin = async (initContext: PluginInitializerContext) => {
|
||||
const { AlertingPlugin } = await import('./plugin');
|
||||
|
|
|
@ -55,7 +55,7 @@ interface AlertOpts {
|
|||
maintenanceWindowIds?: string[];
|
||||
}
|
||||
|
||||
interface ActionOpts {
|
||||
export interface ActionOpts {
|
||||
id: string;
|
||||
typeId: string;
|
||||
alertId?: string;
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry';
|
||||
import { ConnectorAdapter } from '../connector_adapters/types';
|
||||
import { NormalizedSystemAction } from '../rules_client';
|
||||
import { RuleSystemAction } from '../types';
|
||||
import { validateSystemActions } from './validate_system_actions';
|
||||
|
||||
describe('validateSystemActionsWithoutRuleTypeId', () => {
|
||||
const connectorAdapter: ConnectorAdapter = {
|
||||
connectorTypeId: '.test',
|
||||
ruleActionParamsSchema: schema.object({ foo: schema.string() }),
|
||||
buildActionParams: jest.fn(),
|
||||
};
|
||||
|
||||
let registry: ConnectorAdapterRegistry;
|
||||
let actionsClient: jest.Mocked<ActionsClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ConnectorAdapterRegistry();
|
||||
actionsClient = actionsClientMock.create();
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: '.test',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not validate with empty system actions', async () => {
|
||||
const res = await validateSystemActions({
|
||||
connectorAdapterRegistry: registry,
|
||||
systemActions: [],
|
||||
actionsClient,
|
||||
});
|
||||
|
||||
expect(res).toBe(undefined);
|
||||
expect(actionsClient.getBulk).not.toBeCalled();
|
||||
expect(actionsClient.isSystemAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if the action is not a system action even if it is declared as one', async () => {
|
||||
const systemActions: RuleSystemAction[] = [
|
||||
{
|
||||
id: 'not-exist',
|
||||
uuid: '123',
|
||||
params: { foo: 'test' },
|
||||
actionTypeId: '.test',
|
||||
},
|
||||
];
|
||||
|
||||
registry.register(connectorAdapter);
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(false);
|
||||
|
||||
await expect(() =>
|
||||
validateSystemActions({
|
||||
connectorAdapterRegistry: registry,
|
||||
systemActions,
|
||||
actionsClient,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Action not-exist is not a system action"`);
|
||||
});
|
||||
|
||||
it('should throw an error if the action is system action but is not returned from the actions client (getBulk)', async () => {
|
||||
const systemActions: RuleSystemAction[] = [
|
||||
{
|
||||
id: 'not-exist',
|
||||
uuid: '123',
|
||||
params: { foo: 'test' },
|
||||
actionTypeId: '.test',
|
||||
},
|
||||
];
|
||||
|
||||
registry.register(connectorAdapter);
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
|
||||
await expect(() =>
|
||||
validateSystemActions({
|
||||
connectorAdapterRegistry: registry,
|
||||
systemActions,
|
||||
actionsClient,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Action not-exist is not a system action"`);
|
||||
});
|
||||
|
||||
it('should throw an error if the params are not valid', async () => {
|
||||
const systemActions: RuleSystemAction[] = [
|
||||
{
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: { 'not-exist': 'test' },
|
||||
actionTypeId: '.test',
|
||||
},
|
||||
];
|
||||
|
||||
registry.register(connectorAdapter);
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
|
||||
await expect(() =>
|
||||
validateSystemActions({
|
||||
connectorAdapterRegistry: registry,
|
||||
systemActions,
|
||||
actionsClient,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid system action params. System action type: .test - [foo]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the same system action is being used', async () => {
|
||||
const systemActions: RuleSystemAction[] = [
|
||||
{
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: { foo: 'test' },
|
||||
actionTypeId: '.test',
|
||||
},
|
||||
{
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: { foo: 'test' },
|
||||
actionTypeId: '.test',
|
||||
},
|
||||
];
|
||||
|
||||
registry.register(connectorAdapter);
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(false);
|
||||
|
||||
await expect(() =>
|
||||
validateSystemActions({
|
||||
connectorAdapterRegistry: registry,
|
||||
systemActions,
|
||||
actionsClient,
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Cannot use the same system action twice"`);
|
||||
});
|
||||
|
||||
it('should call getBulk correctly', async () => {
|
||||
const systemActions: Array<RuleSystemAction | NormalizedSystemAction> = [
|
||||
{
|
||||
id: 'system_action-id',
|
||||
uuid: '123',
|
||||
params: { foo: 'test' },
|
||||
},
|
||||
{
|
||||
id: 'system_action-id-2',
|
||||
uuid: '123',
|
||||
params: { foo: 'test' },
|
||||
actionTypeId: '.test',
|
||||
},
|
||||
];
|
||||
|
||||
actionsClient.getBulk.mockResolvedValue([
|
||||
{
|
||||
id: 'system_action-id',
|
||||
actionTypeId: '.test',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
{
|
||||
id: 'system_action-id-2',
|
||||
actionTypeId: '.test',
|
||||
config: {},
|
||||
isMissingSecrets: false,
|
||||
name: 'system action connector 2',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
isSystemAction: true,
|
||||
},
|
||||
]);
|
||||
|
||||
registry.register(connectorAdapter);
|
||||
|
||||
actionsClient.isSystemAction.mockReturnValue(true);
|
||||
|
||||
const res = await validateSystemActions({
|
||||
connectorAdapterRegistry: registry,
|
||||
systemActions,
|
||||
actionsClient,
|
||||
});
|
||||
|
||||
expect(res).toBe(undefined);
|
||||
|
||||
expect(actionsClient.getBulk).toBeCalledWith({
|
||||
ids: ['system_action-id', 'system_action-id-2'],
|
||||
throwIfSystemAction: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 Boom from '@hapi/boom';
|
||||
import { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import { ConnectorAdapterRegistry } from '../connector_adapters/connector_adapter_registry';
|
||||
import { bulkValidateConnectorAdapterActionParams } from '../connector_adapters/validate_rule_action_params';
|
||||
import { NormalizedSystemAction } from '../rules_client';
|
||||
import { RuleSystemAction } from '../types';
|
||||
interface Params {
|
||||
actionsClient: ActionsClient;
|
||||
connectorAdapterRegistry: ConnectorAdapterRegistry;
|
||||
systemActions: Array<RuleSystemAction | NormalizedSystemAction>;
|
||||
}
|
||||
|
||||
export const validateSystemActions = async ({
|
||||
actionsClient,
|
||||
connectorAdapterRegistry,
|
||||
systemActions = [],
|
||||
}: Params) => {
|
||||
if (systemActions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* When updating or creating a rule the actions may not contain
|
||||
* the actionTypeId. We need to getBulk using the
|
||||
* actionsClient to get the actionTypeId of each action.
|
||||
* The actionTypeId is needed to get the schema of
|
||||
* the action params using the connector adapter registry
|
||||
*/
|
||||
const actionIds: Set<string> = new Set(systemActions.map((action) => action.id));
|
||||
|
||||
if (actionIds.size !== systemActions.length) {
|
||||
throw Boom.badRequest('Cannot use the same system action twice');
|
||||
}
|
||||
|
||||
const actionResults = await actionsClient.getBulk({
|
||||
ids: Array.from(actionIds),
|
||||
throwIfSystemAction: false,
|
||||
});
|
||||
|
||||
const systemActionsWithActionTypeId: RuleSystemAction[] = [];
|
||||
|
||||
for (const systemAction of systemActions) {
|
||||
const isSystemAction = actionsClient.isSystemAction(systemAction.id);
|
||||
const foundAction = actionResults.find((actionRes) => actionRes.id === systemAction.id);
|
||||
|
||||
if (!isSystemAction || !foundAction) {
|
||||
throw Boom.badRequest(`Action ${systemAction.id} is not a system action`);
|
||||
}
|
||||
|
||||
systemActionsWithActionTypeId.push({
|
||||
...systemAction,
|
||||
actionTypeId: foundAction.actionTypeId,
|
||||
});
|
||||
}
|
||||
|
||||
bulkValidateConnectorAdapterActionParams({
|
||||
connectorAdapterRegistry,
|
||||
actions: systemActionsWithActionTypeId,
|
||||
});
|
||||
};
|
|
@ -36,6 +36,7 @@ const createSetupMock = () => {
|
|||
getContextInitializationPromise: jest.fn(),
|
||||
},
|
||||
getDataStreamAdapter: jest.fn(),
|
||||
registerConnectorAdapter: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
PluginSetup as DataPluginSetup,
|
||||
} from '@kbn/data-plugin/server';
|
||||
import { spacesMock } from '@kbn/spaces-plugin/server/mocks';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { serverlessPluginMock } from '@kbn/serverless/server/mocks';
|
||||
import { AlertsService } from './alerts_service/alerts_service';
|
||||
import { alertsServiceMock } from './alerts_service/alerts_service.mock';
|
||||
|
||||
|
@ -37,7 +39,6 @@ jest.mock('./alerts_service/alerts_service', () => ({
|
|||
import { SharePluginStart } from '@kbn/share-plugin/server';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { generateAlertingConfig } from './test_utils';
|
||||
import { serverlessPluginMock } from '@kbn/serverless/server/mocks';
|
||||
|
||||
const sampleRuleType: RuleType<never, never, {}, never, never, 'default', 'recovered', {}> = {
|
||||
id: 'test',
|
||||
|
@ -240,6 +241,32 @@ describe('Alerting Plugin', () => {
|
|||
expect(ruleType.cancelAlertsOnRuleTimeout).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerConnectorAdapter()', () => {
|
||||
let setup: PluginSetupContract;
|
||||
|
||||
beforeEach(async () => {
|
||||
const context = coreMock.createPluginInitializerContext<AlertingConfig>(
|
||||
generateAlertingConfig()
|
||||
);
|
||||
|
||||
plugin = new AlertingPlugin(context);
|
||||
setup = await plugin.setup(setupMocks, mockPlugins);
|
||||
});
|
||||
|
||||
it('should register a connector adapter', () => {
|
||||
const adapter = {
|
||||
connectorTypeId: '.test',
|
||||
ruleActionParamsSchema: schema.object({}),
|
||||
buildActionParams: jest.fn(),
|
||||
};
|
||||
|
||||
setup.registerConnectorAdapter(adapter);
|
||||
|
||||
// @ts-expect-error: private properties cannot be accessed
|
||||
expect(plugin.connectorAdapterRegistry.get('.test')).toEqual(adapter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('start()', () => {
|
||||
|
|
|
@ -99,6 +99,8 @@ import {
|
|||
} from './alerts_service';
|
||||
import { getRulesSettingsFeature } from './rules_settings_feature';
|
||||
import { maintenanceWindowFeature } from './maintenance_window_feature';
|
||||
import { ConnectorAdapterRegistry } from './connector_adapters/connector_adapter_registry';
|
||||
import { ConnectorAdapter, ConnectorAdapterParams } from './connector_adapters/types';
|
||||
import { DataStreamAdapter, getDataStreamAdapter } from './alerts_service/lib/data_stream_adapter';
|
||||
import { createGetAlertIndicesAliasFn, GetAlertIndicesAlias } from './lib';
|
||||
|
||||
|
@ -118,6 +120,12 @@ export const LEGACY_EVENT_LOG_ACTIONS = {
|
|||
};
|
||||
|
||||
export interface PluginSetupContract {
|
||||
registerConnectorAdapter<
|
||||
RuleActionParams extends ConnectorAdapterParams = ConnectorAdapterParams,
|
||||
ConnectorParams extends ConnectorAdapterParams = ConnectorAdapterParams
|
||||
>(
|
||||
adapter: ConnectorAdapter<RuleActionParams, ConnectorParams>
|
||||
): void;
|
||||
registerType<
|
||||
Params extends RuleTypeParams = RuleTypeParams,
|
||||
ExtractedParams extends RuleTypeParams = RuleTypeParams,
|
||||
|
@ -216,6 +224,7 @@ export class AlertingPlugin {
|
|||
private pluginStop$: Subject<void>;
|
||||
private dataStreamAdapter?: DataStreamAdapter;
|
||||
private nodeRoles: PluginInitializerContext['node']['roles'];
|
||||
private readonly connectorAdapterRegistry = new ConnectorAdapterRegistry();
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.get();
|
||||
|
@ -378,6 +387,14 @@ export class AlertingPlugin {
|
|||
});
|
||||
|
||||
return {
|
||||
registerConnectorAdapter: <
|
||||
RuleActionParams extends ConnectorAdapterParams = ConnectorAdapterParams,
|
||||
ConnectorParams extends ConnectorAdapterParams = ConnectorAdapterParams
|
||||
>(
|
||||
adapter: ConnectorAdapter<RuleActionParams, ConnectorParams>
|
||||
) => {
|
||||
this.connectorAdapterRegistry.register(adapter);
|
||||
},
|
||||
registerType: <
|
||||
Params extends RuleTypeParams = never,
|
||||
ExtractedParams extends RuleTypeParams = never,
|
||||
|
@ -507,6 +524,7 @@ export class AlertingPlugin {
|
|||
maxScheduledPerMinute: this.config.rules.maxScheduledPerMinute,
|
||||
getAlertIndicesAlias: createGetAlertIndicesAliasFn(this.ruleTypeRegistry!),
|
||||
alertsService: this.alertsService,
|
||||
connectorAdapterRegistry: this.connectorAdapterRegistry,
|
||||
uiSettings: core.uiSettings,
|
||||
});
|
||||
|
||||
|
@ -573,6 +591,7 @@ export class AlertingPlugin {
|
|||
usageCounter: this.usageCounter,
|
||||
getRulesSettingsClientWithRequest,
|
||||
getMaintenanceWindowClientWithRequest,
|
||||
connectorAdapterRegistry: this.connectorAdapterRegistry,
|
||||
});
|
||||
|
||||
this.eventLogService!.registerSavedObjectProvider(RULE_SAVED_OBJECT_TYPE, (request) => {
|
||||
|
|
|
@ -9,6 +9,8 @@ import { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server';
|
|||
import { identity } from 'lodash';
|
||||
import type { MethodKeysOf } from '@kbn/utility-types';
|
||||
import { httpServerMock } from '@kbn/core/server/mocks';
|
||||
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import type { ActionsClientMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { rulesClientMock, RulesClientMock } from '../rules_client.mock';
|
||||
import { rulesSettingsClientMock, RulesSettingsClientMock } from '../rules_settings_client.mock';
|
||||
import {
|
||||
|
@ -21,6 +23,7 @@ import type { AlertingRequestHandlerContext } from '../types';
|
|||
export function mockHandlerArguments(
|
||||
{
|
||||
rulesClient = rulesClientMock.create(),
|
||||
actionsClient = actionsClientMock.create(),
|
||||
rulesSettingsClient = rulesSettingsClientMock.create(),
|
||||
maintenanceWindowClient = maintenanceWindowClientMock.create(),
|
||||
listTypes: listTypesRes = [],
|
||||
|
@ -28,6 +31,7 @@ export function mockHandlerArguments(
|
|||
areApiKeysEnabled,
|
||||
}: {
|
||||
rulesClient?: RulesClientMock;
|
||||
actionsClient?: ActionsClientMock;
|
||||
rulesSettingsClient?: RulesSettingsClientMock;
|
||||
maintenanceWindowClient?: MaintenanceWindowClientMock;
|
||||
listTypes?: RuleType[];
|
||||
|
@ -43,6 +47,10 @@ export function mockHandlerArguments(
|
|||
KibanaResponseFactory
|
||||
] {
|
||||
const listTypes = jest.fn(() => listTypesRes);
|
||||
const actionsClientMocked = actionsClient || actionsClientMock.create();
|
||||
|
||||
actionsClient.isSystemAction.mockImplementation((id) => id === 'system_action-id');
|
||||
|
||||
return [
|
||||
{
|
||||
alerting: {
|
||||
|
@ -59,6 +67,11 @@ export function mockHandlerArguments(
|
|||
getFrameworkHealth,
|
||||
areApiKeysEnabled: areApiKeysEnabled ? areApiKeysEnabled : () => Promise.resolve(true),
|
||||
},
|
||||
actions: {
|
||||
getActionsClient() {
|
||||
return actionsClientMocked;
|
||||
},
|
||||
},
|
||||
} as unknown as AlertingRequestHandlerContext,
|
||||
request as KibanaRequest<unknown, unknown, unknown>,
|
||||
mockResponseFactory(response),
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { actionsClientMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { bulkEnableRulesRoute } from './bulk_enable_rules';
|
||||
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 { RuleAction, RuleSystemAction } from '../types';
|
||||
import { Rule } from '../application/rule/types';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
|
||||
|
@ -123,4 +125,121 @@ describe('bulkEnableRulesRoute', () => {
|
|||
|
||||
expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
const mockedRule: Rule<{}> = {
|
||||
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',
|
||||
},
|
||||
],
|
||||
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: RuleAction = {
|
||||
actionTypeId: 'test',
|
||||
group: 'default',
|
||||
id: '2',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
};
|
||||
|
||||
const systemAction: RuleSystemAction = {
|
||||
actionTypeId: 'test-2',
|
||||
id: 'system_action-id',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
};
|
||||
|
||||
const mockedRules: Array<Rule<{}>> = [
|
||||
{
|
||||
...mockedRule,
|
||||
actions: [action],
|
||||
systemActions: [systemAction],
|
||||
},
|
||||
];
|
||||
|
||||
const bulkEnableActionsResult = {
|
||||
rules: mockedRules,
|
||||
errors: [],
|
||||
total: 1,
|
||||
taskIdsFailedToBeEnabled: [],
|
||||
};
|
||||
|
||||
it('should merge actions and systemActions 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');
|
||||
|
||||
bulkEnableRulesRoute({ router, licenseState });
|
||||
const [_, handler] = router.patch.mock.calls[0];
|
||||
|
||||
rulesClient.bulkEnableRules.mockResolvedValueOnce(bulkEnableActionsResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient, actionsClient },
|
||||
{
|
||||
body: bulkEnableRequest,
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
const routeRes = await handler(context, req, res);
|
||||
|
||||
// @ts-expect-error: body exists
|
||||
expect(routeRes.body.rules[0].actions).toEqual([
|
||||
{
|
||||
actionTypeId: 'test',
|
||||
group: 'default',
|
||||
id: '2',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
},
|
||||
{
|
||||
actionTypeId: 'test-2',
|
||||
id: 'system_action-id',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,8 +35,19 @@ export const bulkEnableRulesRoute = ({
|
|||
const { filter, ids } = req.body;
|
||||
|
||||
try {
|
||||
const result = await rulesClient.bulkEnableRules({ filter, ids });
|
||||
return res.ok({ body: result });
|
||||
const bulkEnableResults = await rulesClient.bulkEnableRules({ filter, ids });
|
||||
|
||||
const resultBody = {
|
||||
body: {
|
||||
...bulkEnableResults,
|
||||
// TODO We need to fix this API to return snake case like every other API
|
||||
rules: bulkEnableResults.rules.map(({ actions, systemActions, ...rule }) => {
|
||||
return { ...rule, actions: [...actions, ...(systemActions ?? [])] };
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return res.ok(resultBody);
|
||||
} catch (e) {
|
||||
if (e instanceof RuleTypeDisabledError) {
|
||||
return e.sendResponse(res);
|
||||
|
|
|
@ -13,8 +13,7 @@ import { mockHandlerArguments } from './_mock_handler_arguments';
|
|||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled';
|
||||
import { cloneRuleRoute } from './clone_rule';
|
||||
import { SanitizedRule } from '../types';
|
||||
import { AsApiContract } from './lib';
|
||||
import { RuleAction, RuleSystemAction, SanitizedRule } from '../types';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
jest.mock('../lib/license_api_access', () => ({
|
||||
|
@ -29,6 +28,25 @@ describe('cloneRuleRoute', () => {
|
|||
const createdAt = new Date();
|
||||
const updatedAt = new Date();
|
||||
|
||||
const action: RuleAction = {
|
||||
actionTypeId: 'test',
|
||||
group: 'default',
|
||||
id: '2',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
};
|
||||
|
||||
const systemAction: RuleSystemAction = {
|
||||
actionTypeId: 'test-2',
|
||||
id: 'system_action-id',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
};
|
||||
|
||||
const mockedRule: SanitizedRule<{ bar: boolean }> = {
|
||||
alertTypeId: '1',
|
||||
consumer: 'bar',
|
||||
|
@ -39,17 +57,7 @@ describe('cloneRuleRoute', () => {
|
|||
bar: true,
|
||||
},
|
||||
throttle: '30s',
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: 'test',
|
||||
group: 'default',
|
||||
id: '2',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
},
|
||||
],
|
||||
actions: [action],
|
||||
enabled: true,
|
||||
muteAll: false,
|
||||
createdBy: '',
|
||||
|
@ -80,7 +88,7 @@ describe('cloneRuleRoute', () => {
|
|||
],
|
||||
};
|
||||
|
||||
const cloneResult: AsApiContract<SanitizedRule<{ bar: boolean }>> = {
|
||||
const cloneResult = {
|
||||
...ruleToClone,
|
||||
mute_all: mockedRule.muteAll,
|
||||
created_by: mockedRule.createdBy,
|
||||
|
@ -214,4 +222,54 @@ describe('cloneRuleRoute', () => {
|
|||
|
||||
expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
|
||||
});
|
||||
|
||||
it('transforms the system actions in the response of the rules client correctly', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
cloneRuleRoute(router, licenseState);
|
||||
|
||||
const [_, handler] = router.post.mock.calls[0];
|
||||
|
||||
rulesClient.clone.mockResolvedValueOnce({
|
||||
...mockedRule,
|
||||
actions: [action],
|
||||
systemActions: [systemAction],
|
||||
});
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
const routeRes = await handler(context, req, res);
|
||||
|
||||
// @ts-expect-error: body exists
|
||||
expect(routeRes.body.systemActions).toBeUndefined();
|
||||
// @ts-expect-error: body exists
|
||||
expect(routeRes.body.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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,27 +8,23 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { ILicenseState, RuleTypeDisabledError } from '../lib';
|
||||
import {
|
||||
verifyAccessAndContext,
|
||||
RewriteResponseCase,
|
||||
handleDisabledApiKeysError,
|
||||
rewriteRuleLastRun,
|
||||
rewriteActionsRes,
|
||||
} from './lib';
|
||||
import { verifyAccessAndContext, handleDisabledApiKeysError, rewriteRuleLastRun } from './lib';
|
||||
import {
|
||||
RuleTypeParams,
|
||||
AlertingRequestHandlerContext,
|
||||
INTERNAL_BASE_ALERTING_API_PATH,
|
||||
PartialRule,
|
||||
} from '../types';
|
||||
import { transformRuleActions } from './rule/transforms';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
newId: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
|
||||
const rewriteBodyRes = ({
|
||||
actions,
|
||||
systemActions,
|
||||
alertTypeId,
|
||||
scheduledTaskId,
|
||||
createdBy,
|
||||
|
@ -46,7 +42,7 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
|
|||
lastRun,
|
||||
nextRun,
|
||||
...rest
|
||||
}) => ({
|
||||
}: PartialRule<RuleTypeParams>) => ({
|
||||
...rest,
|
||||
api_key_owner: apiKeyOwner,
|
||||
created_by: createdBy,
|
||||
|
@ -71,7 +67,7 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
|
|||
: {}),
|
||||
...(actions
|
||||
? {
|
||||
actions: rewriteActionsRes(actions),
|
||||
actions: transformRuleActions(actions, systemActions),
|
||||
}
|
||||
: {}),
|
||||
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
|
||||
|
|
|
@ -97,6 +97,219 @@ describe('findRulesRoute', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should rewrite the rule and actions correctly', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findRulesRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rules/_find"`);
|
||||
|
||||
const findResult = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [
|
||||
{
|
||||
id: '3d534c70-582b-11ec-8995-2b1578a3bc5d',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
alertTypeId: '.index-threshold',
|
||||
name: 'stressing index-threshold 37/200',
|
||||
consumer: 'alerts',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
throttle: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: '2889684073',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: '2889684073',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
schedule: {
|
||||
interval: '1s',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: '.server-log',
|
||||
params: {
|
||||
message: 'alert 37: {{context.message}}',
|
||||
},
|
||||
group: 'threshold met',
|
||||
id: '3619a0d0-582b-11ec-8995-2b1578a3bc5d',
|
||||
uuid: '123-456',
|
||||
},
|
||||
],
|
||||
systemActions: [
|
||||
{ actionTypeId: '.test', id: 'system_action-id', params: {}, uuid: '789' },
|
||||
],
|
||||
params: { x: 42 },
|
||||
updatedAt: '2024-03-21T13:15:00.498Z',
|
||||
createdAt: '2024-03-21T13:15:00.498Z',
|
||||
scheduledTaskId: '52125fb0-5895-11ec-ae69-bb65d1a71b72',
|
||||
executionStatus: {
|
||||
status: 'ok' as const,
|
||||
lastExecutionDate: '2024-03-21T13:15:00.498Z',
|
||||
lastDuration: 1194,
|
||||
},
|
||||
revision: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error: TS complains about group being undefined in the system action
|
||||
rulesClient.find.mockResolvedValueOnce(findResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
query: {
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
default_search_operator: 'OR',
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"data": Array [
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"connector_type_id": ".server-log",
|
||||
"group": "threshold met",
|
||||
"id": "3619a0d0-582b-11ec-8995-2b1578a3bc5d",
|
||||
"params": Object {
|
||||
"message": "alert 37: {{context.message}}",
|
||||
},
|
||||
"uuid": "123-456",
|
||||
},
|
||||
Object {
|
||||
"connector_type_id": ".test",
|
||||
"id": "system_action-id",
|
||||
"params": Object {},
|
||||
"uuid": "789",
|
||||
},
|
||||
],
|
||||
"apiKey": null,
|
||||
"api_key_owner": "2889684073",
|
||||
"consumer": "alerts",
|
||||
"created_at": "2024-03-21T13:15:00.498Z",
|
||||
"created_by": "elastic",
|
||||
"enabled": true,
|
||||
"execution_status": Object {
|
||||
"last_duration": 1194,
|
||||
"last_execution_date": "2024-03-21T13:15:00.498Z",
|
||||
"status": "ok",
|
||||
},
|
||||
"id": "3d534c70-582b-11ec-8995-2b1578a3bc5d",
|
||||
"mute_all": false,
|
||||
"muted_alert_ids": Array [],
|
||||
"name": "stressing index-threshold 37/200",
|
||||
"notify_when": "onActiveAlert",
|
||||
"params": Object {
|
||||
"x": 42,
|
||||
},
|
||||
"revision": 0,
|
||||
"rule_type_id": ".index-threshold",
|
||||
"schedule": Object {
|
||||
"interval": "1s",
|
||||
},
|
||||
"scheduled_task_id": "52125fb0-5895-11ec-ae69-bb65d1a71b72",
|
||||
"snooze_schedule": undefined,
|
||||
"tags": Array [],
|
||||
"throttle": null,
|
||||
"updated_at": "2024-03-21T13:15:00.498Z",
|
||||
"updated_by": "2889684073",
|
||||
},
|
||||
],
|
||||
"page": 1,
|
||||
"per_page": 1,
|
||||
"total": 0,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(rulesClient.find).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.find.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"excludeFromPublicApi": true,
|
||||
"includeSnoozeData": true,
|
||||
"options": Object {
|
||||
"defaultSearchOperator": "OR",
|
||||
"filterConsumers": undefined,
|
||||
"page": 1,
|
||||
"perPage": 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
total: 0,
|
||||
data: [
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
connector_type_id: '.server-log',
|
||||
group: 'threshold met',
|
||||
id: '3619a0d0-582b-11ec-8995-2b1578a3bc5d',
|
||||
params: {
|
||||
message: 'alert 37: {{context.message}}',
|
||||
},
|
||||
uuid: '123-456',
|
||||
},
|
||||
{
|
||||
connector_type_id: '.test',
|
||||
id: 'system_action-id',
|
||||
params: {},
|
||||
uuid: '789',
|
||||
},
|
||||
],
|
||||
apiKey: null,
|
||||
api_key_owner: '2889684073',
|
||||
consumer: 'alerts',
|
||||
created_at: '2024-03-21T13:15:00.498Z',
|
||||
created_by: 'elastic',
|
||||
enabled: true,
|
||||
execution_status: {
|
||||
last_duration: 1194,
|
||||
last_execution_date: '2024-03-21T13:15:00.498Z',
|
||||
status: 'ok',
|
||||
},
|
||||
id: '3d534c70-582b-11ec-8995-2b1578a3bc5d',
|
||||
mute_all: false,
|
||||
muted_alert_ids: [],
|
||||
name: 'stressing index-threshold 37/200',
|
||||
notify_when: 'onActiveAlert',
|
||||
params: {
|
||||
x: 42,
|
||||
},
|
||||
revision: 0,
|
||||
rule_type_id: '.index-threshold',
|
||||
schedule: {
|
||||
interval: '1s',
|
||||
},
|
||||
scheduled_task_id: '52125fb0-5895-11ec-ae69-bb65d1a71b72',
|
||||
snooze_schedule: undefined,
|
||||
tags: [],
|
||||
throttle: null,
|
||||
updated_at: '2024-03-21T13:15:00.498Z',
|
||||
updated_by: '2889684073',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures the license allows finding rules', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
|
|
@ -10,12 +10,7 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { FindOptions, FindResult } from '../rules_client';
|
||||
import {
|
||||
RewriteRequestCase,
|
||||
RewriteResponseCase,
|
||||
verifyAccessAndContext,
|
||||
rewriteRule,
|
||||
} from './lib';
|
||||
import { RewriteRequestCase, verifyAccessAndContext, rewriteRule } from './lib';
|
||||
import {
|
||||
RuleTypeParams,
|
||||
AlertingRequestHandlerContext,
|
||||
|
@ -69,11 +64,7 @@ const rewriteQueryReq: RewriteRequestCase<FindOptions> = ({
|
|||
...(hasReference ? { hasReference } : {}),
|
||||
...(searchFields ? { searchFields } : {}),
|
||||
});
|
||||
const rewriteBodyRes: RewriteResponseCase<FindResult<RuleTypeParams>> = ({
|
||||
perPage,
|
||||
data,
|
||||
...restOfResult
|
||||
}) => {
|
||||
const rewriteBodyRes = ({ perPage, data, ...restOfResult }: FindResult<RuleTypeParams>) => {
|
||||
return {
|
||||
...restOfResult,
|
||||
per_page: perPage,
|
||||
|
|
|
@ -12,8 +12,7 @@ import { licenseStateMock } from '../lib/license_state.mock';
|
|||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { SanitizedRule } from '../types';
|
||||
import { AsApiContract } from './lib';
|
||||
import { RuleAction, RuleSystemAction, SanitizedRule } from '../types';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
jest.mock('../lib/license_api_access', () => ({
|
||||
|
@ -25,6 +24,37 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
describe('getRuleRoute', () => {
|
||||
const action: RuleAction = {
|
||||
group: 'default',
|
||||
id: '2',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
alertsFilter: {
|
||||
query: {
|
||||
kql: 'name:test',
|
||||
dsl: '{"must": {"term": { "name": "test" }}}',
|
||||
filters: [],
|
||||
},
|
||||
timeframe: {
|
||||
days: [1],
|
||||
hours: { start: '08:00', end: '17:00' },
|
||||
timezone: 'UTC',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const systemAction: RuleSystemAction = {
|
||||
actionTypeId: 'test-2',
|
||||
id: 'system_action-id',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
};
|
||||
|
||||
const mockedAlert: SanitizedRule<{
|
||||
bar: boolean;
|
||||
}> = {
|
||||
|
@ -36,30 +66,7 @@ describe('getRuleRoute', () => {
|
|||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '2',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
alertsFilter: {
|
||||
query: {
|
||||
kql: 'name:test',
|
||||
// @ts-expect-error upgrade typescript v4.9.5
|
||||
dsl: '{"must": {"term": { "name": "test" }}}',
|
||||
filters: [],
|
||||
},
|
||||
timeframe: {
|
||||
days: [1],
|
||||
hours: { start: '08:00', end: '17:00' },
|
||||
timezone: 'UTC',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: [action],
|
||||
consumer: 'bar',
|
||||
name: 'abc',
|
||||
tags: ['foo'],
|
||||
|
@ -78,7 +85,8 @@ describe('getRuleRoute', () => {
|
|||
revision: 0,
|
||||
};
|
||||
|
||||
const getResult: AsApiContract<SanitizedRule<{ bar: boolean }>> = {
|
||||
const mockedAction0 = mockedAlert.actions[0];
|
||||
const getResult = {
|
||||
...pick(mockedAlert, 'consumer', 'name', 'schedule', 'tags', 'params', 'throttle', 'enabled'),
|
||||
rule_type_id: mockedAlert.alertTypeId,
|
||||
notify_when: mockedAlert.notifyWhen,
|
||||
|
@ -97,12 +105,12 @@ describe('getRuleRoute', () => {
|
|||
},
|
||||
actions: [
|
||||
{
|
||||
group: mockedAlert.actions[0].group,
|
||||
id: mockedAlert.actions[0].id,
|
||||
params: mockedAlert.actions[0].params,
|
||||
connector_type_id: mockedAlert.actions[0].actionTypeId,
|
||||
uuid: mockedAlert.actions[0].uuid,
|
||||
alerts_filter: mockedAlert.actions[0].alertsFilter,
|
||||
group: mockedAction0.group,
|
||||
id: mockedAction0.id,
|
||||
params: mockedAction0.params,
|
||||
connector_type_id: mockedAction0.actionTypeId,
|
||||
uuid: mockedAction0.uuid,
|
||||
alerts_filter: mockedAction0.alertsFilter,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -184,4 +192,66 @@ describe('getRuleRoute', () => {
|
|||
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
it('transforms the system actions in the response of the rules client correctly', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getRuleRoute(router, licenseState);
|
||||
const [_, handler] = router.get.mock.calls[0];
|
||||
|
||||
rulesClient.get.mockResolvedValueOnce({
|
||||
...mockedAlert,
|
||||
actions: [action],
|
||||
systemActions: [systemAction],
|
||||
});
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
const routeRes = await handler(context, req, res);
|
||||
|
||||
// @ts-expect-error: body exists
|
||||
expect(routeRes.body.systemActions).toBeUndefined();
|
||||
// @ts-expect-error: body exists
|
||||
expect(routeRes.body.actions).toEqual([
|
||||
{
|
||||
alerts_filter: {
|
||||
query: {
|
||||
dsl: '{"must": {"term": { "name": "test" }}}',
|
||||
filters: [],
|
||||
kql: 'name:test',
|
||||
},
|
||||
timeframe: {
|
||||
days: [1],
|
||||
hours: {
|
||||
end: '17:00',
|
||||
start: '08:00',
|
||||
},
|
||||
timezone: 'UTC',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,12 +9,7 @@ import { omit } from 'lodash';
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { ILicenseState } from '../lib';
|
||||
import {
|
||||
verifyAccessAndContext,
|
||||
RewriteResponseCase,
|
||||
rewriteRuleLastRun,
|
||||
rewriteActionsRes,
|
||||
} from './lib';
|
||||
import { verifyAccessAndContext, rewriteRuleLastRun } from './lib';
|
||||
import {
|
||||
RuleTypeParams,
|
||||
AlertingRequestHandlerContext,
|
||||
|
@ -22,12 +17,13 @@ import {
|
|||
INTERNAL_BASE_ALERTING_API_PATH,
|
||||
SanitizedRule,
|
||||
} from '../types';
|
||||
import { transformRuleActions } from './rule/transforms';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
|
||||
const rewriteBodyRes = ({
|
||||
alertTypeId,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
|
@ -40,6 +36,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
|
|||
mutedInstanceIds,
|
||||
executionStatus,
|
||||
actions,
|
||||
systemActions,
|
||||
scheduledTaskId,
|
||||
snoozeSchedule,
|
||||
isSnoozedUntil,
|
||||
|
@ -47,7 +44,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
|
|||
nextRun,
|
||||
viewInAppRelativeUrl,
|
||||
...rest
|
||||
}) => ({
|
||||
}: SanitizedRule<RuleTypeParams>) => ({
|
||||
...rest,
|
||||
rule_type_id: alertTypeId,
|
||||
created_by: createdBy,
|
||||
|
@ -66,7 +63,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
|
|||
last_execution_date: executionStatus.lastExecutionDate,
|
||||
last_duration: executionStatus.lastDuration,
|
||||
},
|
||||
actions: rewriteActionsRes(actions),
|
||||
actions: transformRuleActions(actions, systemActions),
|
||||
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
|
||||
...(nextRun ? { next_run: nextRun } : {}),
|
||||
...(viewInAppRelativeUrl ? { view_in_app_relative_url: viewInAppRelativeUrl } : {}),
|
||||
|
|
|
@ -12,7 +12,7 @@ import { licenseStateMock } from '../../lib/license_state.mock';
|
|||
import { verifyApiAccess } from '../../lib/license_api_access';
|
||||
import { mockHandlerArguments } from '../_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../../rules_client.mock';
|
||||
import { Rule } from '../../../common/rule';
|
||||
import { Rule, RuleSystemAction } from '../../../common/rule';
|
||||
import { RuleTypeDisabledError } from '../../lib/errors/rule_type_disabled';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
|
||||
|
@ -57,6 +57,15 @@ describe('createAlertRoute', () => {
|
|||
],
|
||||
};
|
||||
|
||||
const systemAction: RuleSystemAction = {
|
||||
actionTypeId: 'test-2',
|
||||
id: 'system_action-id',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
uuid: '123-456',
|
||||
};
|
||||
|
||||
const createResult: Rule<{ bar: boolean }> = {
|
||||
...mockedAlert,
|
||||
enabled: true,
|
||||
|
@ -460,8 +469,81 @@ describe('createAlertRoute', () => {
|
|||
usageCounter: mockUsageCounter,
|
||||
});
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
rulesClient.create.mockResolvedValueOnce(createResult);
|
||||
const [context, req, res] = mockHandlerArguments({ rulesClient }, {}, ['ok']);
|
||||
await handler(context, req, res);
|
||||
expect(trackLegacyRouteUsage).toHaveBeenCalledWith('create', mockUsageCounter);
|
||||
});
|
||||
|
||||
it('does not return system actions', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
|
||||
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
|
||||
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
|
||||
|
||||
createAlertRoute({
|
||||
router,
|
||||
licenseState,
|
||||
encryptedSavedObjects,
|
||||
usageCounter: mockUsageCounter,
|
||||
});
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id?}"`);
|
||||
|
||||
rulesClient.create.mockResolvedValueOnce({ ...createResult, systemActions: [systemAction] });
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
body: mockedAlert,
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toEqual({ body: createResult });
|
||||
|
||||
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
|
||||
expect(rulesClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "2",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "1",
|
||||
"consumer": "bar",
|
||||
"name": "abc",
|
||||
"notifyWhen": "onActionGroupChange",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"schedule": Object {
|
||||
"interval": "10s",
|
||||
},
|
||||
"tags": Array [
|
||||
"foo",
|
||||
],
|
||||
"throttle": "30s",
|
||||
},
|
||||
"options": Object {
|
||||
"id": undefined,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: createResult,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -78,10 +78,11 @@ export const createAlertRoute = ({ router, licenseState, usageCounter }: RouteOp
|
|||
});
|
||||
|
||||
try {
|
||||
const alertRes: SanitizedRule<RuleTypeParams> = await rulesClient.create<RuleTypeParams>({
|
||||
data: { ...alert, notifyWhen },
|
||||
options: { id: params?.id },
|
||||
});
|
||||
const { systemActions, ...alertRes }: SanitizedRule<RuleTypeParams> =
|
||||
await rulesClient.create<RuleTypeParams>({
|
||||
data: { ...alert, notifyWhen },
|
||||
options: { id: params?.id },
|
||||
});
|
||||
return res.ok({
|
||||
body: alertRes,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
|
||||
import { findAlertRoute } from './find';
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
|
@ -160,6 +162,13 @@ describe('findAlertRoute', () => {
|
|||
|
||||
findAlertRoute(router, licenseState, mockUsageCounter);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
const findResult = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
rulesClient.find.mockResolvedValueOnce(findResult);
|
||||
const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, query: {} }, [
|
||||
'ok',
|
||||
]);
|
||||
|
@ -175,6 +184,14 @@ describe('findAlertRoute', () => {
|
|||
|
||||
findAlertRoute(router, licenseState, mockUsageCounter);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const findResult = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
rulesClient.find.mockResolvedValueOnce(findResult);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
|
@ -204,6 +221,13 @@ describe('findAlertRoute', () => {
|
|||
|
||||
findAlertRoute(router, licenseState, mockUsageCounter);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
const findResult = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
rulesClient.find.mockResolvedValueOnce(findResult);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
|
@ -221,4 +245,153 @@ describe('findAlertRoute', () => {
|
|||
incrementBy: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return system actions', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findAlertRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`);
|
||||
|
||||
const findResult = {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
total: 0,
|
||||
data: [
|
||||
{
|
||||
id: '3d534c70-582b-11ec-8995-2b1578a3bc5d',
|
||||
notifyWhen: 'onActiveAlert' as const,
|
||||
alertTypeId: '.index-threshold',
|
||||
name: 'stressing index-threshold 37/200',
|
||||
consumer: 'alerts',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
throttle: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: '2889684073',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: '2889684073',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
schedule: {
|
||||
interval: '1s',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
actionTypeId: '.server-log',
|
||||
params: {
|
||||
message: 'alert 37: {{context.message}}',
|
||||
},
|
||||
group: 'threshold met',
|
||||
id: '3619a0d0-582b-11ec-8995-2b1578a3bc5d',
|
||||
uuid: '123-456',
|
||||
},
|
||||
],
|
||||
systemActions: [
|
||||
{ actionTypeId: '.test', id: 'system_action-id', params: {}, uuid: '789' },
|
||||
],
|
||||
params: { x: 42 },
|
||||
updatedAt: '2024-03-21T13:15:00.498Z',
|
||||
createdAt: '2024-03-21T13:15:00.498Z',
|
||||
scheduledTaskId: '52125fb0-5895-11ec-ae69-bb65d1a71b72',
|
||||
executionStatus: {
|
||||
status: 'ok' as const,
|
||||
lastExecutionDate: '2024-03-21T13:15:00.498Z',
|
||||
lastDuration: 1194,
|
||||
},
|
||||
revision: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-expect-error: TS complains about dates being string and not a Date object
|
||||
rulesClient.find.mockResolvedValueOnce(findResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
query: {
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
default_search_operator: 'OR',
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"data": Array [
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionTypeId": ".server-log",
|
||||
"group": "threshold met",
|
||||
"id": "3619a0d0-582b-11ec-8995-2b1578a3bc5d",
|
||||
"params": Object {
|
||||
"message": "alert 37: {{context.message}}",
|
||||
},
|
||||
"uuid": "123-456",
|
||||
},
|
||||
],
|
||||
"alertTypeId": ".index-threshold",
|
||||
"apiKey": null,
|
||||
"apiKeyOwner": "2889684073",
|
||||
"consumer": "alerts",
|
||||
"createdAt": "2024-03-21T13:15:00.498Z",
|
||||
"createdBy": "elastic",
|
||||
"enabled": true,
|
||||
"executionStatus": Object {
|
||||
"lastDuration": 1194,
|
||||
"lastExecutionDate": "2024-03-21T13:15:00.498Z",
|
||||
"status": "ok",
|
||||
},
|
||||
"id": "3d534c70-582b-11ec-8995-2b1578a3bc5d",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": Array [],
|
||||
"name": "stressing index-threshold 37/200",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"params": Object {
|
||||
"x": 42,
|
||||
},
|
||||
"revision": 0,
|
||||
"schedule": Object {
|
||||
"interval": "1s",
|
||||
},
|
||||
"scheduledTaskId": "52125fb0-5895-11ec-ae69-bb65d1a71b72",
|
||||
"tags": Array [],
|
||||
"throttle": null,
|
||||
"updatedAt": "2024-03-21T13:15:00.498Z",
|
||||
"updatedBy": "2889684073",
|
||||
},
|
||||
],
|
||||
"page": 1,
|
||||
"perPage": 1,
|
||||
"total": 0,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(rulesClient.find).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.find.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"excludeFromPublicApi": true,
|
||||
"options": Object {
|
||||
"defaultSearchOperator": "OR",
|
||||
"page": 1,
|
||||
"perPage": 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: omit(findResult, 'data[0].systemActions'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -99,7 +99,10 @@ export const findAlertRoute = (
|
|||
|
||||
const findResult = await rulesClient.find({ options, excludeFromPublicApi: true });
|
||||
return res.ok({
|
||||
body: findResult,
|
||||
body: {
|
||||
...findResult,
|
||||
data: findResult.data.map(({ systemActions, ...rule }) => rule),
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue