Skip to content

Commit

Permalink
[Actions] System actions MVP (#166267)
Browse files Browse the repository at this point in the history
## 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 #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>
  • Loading branch information
7 people committed Apr 2, 2024
1 parent 0dfa0d0 commit 26d8222
Show file tree
Hide file tree
Showing 264 changed files with 13,607 additions and 1,937 deletions.
9 changes: 4 additions & 5 deletions x-pack/examples/triggers_actions_ui_example/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
"owner": "@elastic/response-ops",
"plugin": {
"id": "triggersActionsUiExample",
"server": false,
"server": true,
"browser": true,
"requiredPlugins": [
"triggersActionsUi",
"data",
"alerting",
"developerExamples",
"kibanaReact",
"cases"
],
"optionalPlugins": [
"spaces"
"cases",
"actions"
],
"optionalPlugins": ["spaces"]
}
}
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions x-pack/examples/triggers_actions_ui_example/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -145,6 +146,8 @@ export class TriggersActionsUiExamplePlugin
};

alertsTableConfigurationRegistry.register(config);

triggersActionsUi.actionTypeRegistry.register(getSystemLogExampleConnectorType());
}

public stop() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions x-pack/examples/triggers_actions_ui_example/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { PluginInitializer } from '@kbn/core/server';

export const plugin: PluginInitializer<void, void> = async () => {
const { TriggersActionsUiExamplePlugin } = await import('./plugin');
return new TriggersActionsUiExamplePlugin();
};
33 changes: 33 additions & 0 deletions x-pack/examples/triggers_actions_ui_example/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -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() {}
}
3 changes: 3 additions & 0 deletions x-pack/examples/triggers_actions_ui_example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@
"@kbn/data-plugin",
"@kbn/i18n-react",
"@kbn/shared-ux-router",
"@kbn/i18n",
"@kbn/actions-plugin",
"@kbn/config-schema",
]
}
4 changes: 2 additions & 2 deletions x-pack/plugins/actions/server/action_type_registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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 () => {
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2797,7 +2797,7 @@ describe('execute()', () => {
});

expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
actionTypeId: '.cases',
operation: 'execute',
additionalPrivileges: ['test/create'],
});
Expand Down Expand Up @@ -2930,7 +2930,7 @@ describe('execute()', () => {
});

expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
actionTypeId: '.cases',
operation: 'execute',
additionalPrivileges: ['test/create'],
});
Expand Down
Loading

0 comments on commit 26d8222

Please sign in to comment.