Skip to content

Commit

Permalink
Change action config / params schema from joi to @kbn/config-schema (#…
Browse files Browse the repository at this point in the history
…40694)

* Adds actionTypeConfig to AAD exclusion for action ESOs

fixes #40177

Prior to this, the `actionTypeConfig` was not excluded from AAD when using
encrypted saved objects in actions.

https://github.com/elastic/kibana/blob/d0da71c2b4b154fe2efe86b44869c06709c15d14/x-pack/legacy/plugins/actions/server/init.ts#L31-L35

This caused a problem when updating values in the `actionTypeConfig`, as per
issue #40177

Also added `x-pack/test/functional/es_archives/actions/README.md` to explain
how to get the id and encrypted value string, if this needs to be done again
later, since it's a little tricky.

* change alertings reference to actions archived action

Alert happened to reuse the archived action, so it's reference to the
action also had to be updated.
  • Loading branch information
pmuellr committed Jul 15, 2019
1 parent f0e7b9e commit ec8ec82
Show file tree
Hide file tree
Showing 23 changed files with 510 additions and 337 deletions.
58 changes: 18 additions & 40 deletions x-pack/legacy/plugins/actions/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# Kibana actions

The Kibana actions plugin provides a common place to execute actions. You can:
The Kibana actions plugin provides a framework to create executable actions. You can:

- Register an action type
- View a list of registered action types
- Fire an action either manually or by using an alert
- Perform CRUD on actions with encrypted configurations
- Register an action type and associate a JavaScript function to run when actions
are executed.
- Get a list of registered action types
- Create an action from an action type and encrypted configuration object.
- Get a list of actions that have been created.
- Execute an action, passing it a parameter object.
- Perform CRUD operations on actions.

## Terminology

**Action Type**: A programatically defined integration with another service, with an expected set of configuration and parameters properties.
**Action Type**: A programatically defined integration with another service, with an expected set of configuration and parameters properties, typically defined with a schema. Plugins can add new
action types.

**Action**: A user-defined configuration that satisfies an action type's expected configuration.
**Action**: A configuration object associated with an action type, that is ready to be executed. The configuration is persisted via Saved Objects, and some/none/all of the configuration properties can be stored encrypted.

## Usage

Expand All @@ -32,10 +36,12 @@ The following table describes the properties of the `options` object.
|id|Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `<plugin_id>.mySpecialAction` for your action types.|string|
|name|A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types.|string|
|unencryptedAttributes|A list of opt-out attributes that don't need to be encrypted. These attributes won't need to be re-entered on import / export when the feature becomes available. These attributes will also be readable / displayed when it comes to a table / edit screen.|array of strings|
|validate.params|When developing an action type, it needs to accept parameters to know what to do with the action. (Example to, from, subject, body of an email). Use joi object validation if you would like `params` to be validated before being passed to the executor. <p>Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns an object `{error, value}`, where error is a validation error, and value is the sanitized version of the input object.|Joi schema|
|validate.config|Similar to params, a config is required when creating an action (for example host, port, username, and password of an email server). Use the joi object validation if you would like the config to be validated before being passed to the executor.|Joi schema|
|validate.params|When developing an action type, it needs to accept parameters to know what to do with the action. (Example to, from, subject, body of an email). See the current built-in email action type for an example of the state-of-the-art validation. <p>Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message|schema / validation function|
|validate.config|Similar to params, a config is required when creating an action (for example host, port, username, and password of an email server). |schema / validation function|
|executor|This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below.|Function|

**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur.

### Executor

This is the primary function for an action type. Whenever the action needs to execute, this function will perform the action. It receives a variety of parameters. The following table describes the properties that the executor receives.
Expand All @@ -52,37 +58,9 @@ This is the primary function for an action type. Whenever the action needs to ex

### Example

Below is an example email action type. The attributes `host` and `port` are configured to be unencrypted by using the `unencryptedAttributes` attribute.
The built-in email action type provides a good example of creating an action type with non-trivial configuration and params:
[x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts](server/builtin_action_types/email.ts)

```
server.plugins.actions.registerType({
id: 'smtp',
name: 'Email',
unencryptedAttributes: ['host', 'port'],
validate: {
params: Joi.object()
.keys({
to: Joi.array().items(Joi.string()).required(),
from: Joi.string().required(),
subject: Joi.string().required(),
body: Joi.string().required(),
})
.required(),
config: Joi.object()
.keys({
host: Joi.string().required(),
port: Joi.number().default(465),
username: Joi.string().required(),
password: Joi.string().required(),
})
.required(),
},
async executor({ config, params, services }) {
const transporter = nodemailer. createTransport(config);
await transporter.sendMail(params);
},
});
```

## RESTful API

Expand Down Expand Up @@ -223,7 +201,7 @@ This action type uses [nodemailer](https://nodemailer.com/about/) to send emails

Either the property `service` must be provided, or the `host` and `port` properties must be provided. If `service` is provided, `host`, `port` and `secure` are ignored. For more information on the `gmail` service value specifically, see the [nodemailer gmail documentation](https://nodemailer.com/usage/using-gmail/).

The `security` property defaults to `false`. See the [nodemailer TLS documentation](https://nodemailer.com/smtp/#tls-options) for more information.
The `secure` property defaults to `false`. See the [nodemailer TLS documentation](https://nodemailer.com/smtp/#tls-options) for more information.

The `from` field can be specified as in typical `"user@host-name"` format, or as `"human name <user@host-name>"` format. See the [nodemailer address documentation](https://nodemailer.com/message/addresses/) for more information.

Expand Down
183 changes: 90 additions & 93 deletions x-pack/legacy/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

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

import { ActionTypeRegistry } from './action_type_registry';
import { ActionsClient } from './actions_client';
import { taskManagerMock } from '../../task_manager/task_manager.mock';
Expand Down Expand Up @@ -69,20 +70,20 @@ describe('create()', () => {
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
Object {
"actionTypeConfig": Object {},
"actionTypeConfigSecrets": Object {},
"actionTypeId": "my-action-type",
"description": "my description",
},
Object {
"migrationVersion": Object {},
"references": Array [],
},
]
`);
Array [
"action",
Object {
"actionTypeConfig": Object {},
"actionTypeConfigSecrets": Object {},
"actionTypeId": "my-action-type",
"description": "my description",
},
Object {
"migrationVersion": Object {},
"references": Array [],
},
]
`);
});

test('validates actionTypeConfig', async () => {
Expand All @@ -96,11 +97,9 @@ Array [
name: 'My action type',
unencryptedAttributes: [],
validate: {
config: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
config: schema.object({
param1: schema.string(),
}),
},
async executor() {},
});
Expand All @@ -113,7 +112,7 @@ Array [
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"The following actionTypeConfig attributes are invalid: param1 [any.required]"`
`"The actionTypeConfig is invalid: [param1]: expected value of type [string] but got [undefined]"`
);
});

Expand Down Expand Up @@ -169,22 +168,22 @@ Array [
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
Object {
"actionTypeConfig": Object {
"a": true,
"c": true,
},
"actionTypeConfigSecrets": Object {
"b": true,
},
"actionTypeId": "my-action-type",
"description": "my description",
},
undefined,
]
`);
Array [
"action",
Object {
"actionTypeConfig": Object {
"a": true,
"c": true,
},
"actionTypeConfigSecrets": Object {
"b": true,
},
"actionTypeId": "my-action-type",
"description": "my description",
},
undefined,
]
`);
});
});

Expand All @@ -206,11 +205,11 @@ describe('get()', () => {
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"1",
]
`);
Array [
"action",
"1",
]
`);
});
});

Expand Down Expand Up @@ -239,12 +238,12 @@ describe('find()', () => {
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"type": "action",
},
]
`);
Array [
Object {
"type": "action",
},
]
`);
});
});

Expand All @@ -261,11 +260,11 @@ describe('delete()', () => {
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"1",
]
`);
Array [
"action",
"1",
]
`);
});
});

Expand Down Expand Up @@ -308,25 +307,25 @@ describe('update()', () => {
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"my-action",
Object {
"actionTypeConfig": Object {},
"actionTypeConfigSecrets": Object {},
"actionTypeId": "my-action-type",
"description": "my description",
},
Object {},
]
`);
Array [
"action",
"my-action",
Object {
"actionTypeConfig": Object {},
"actionTypeConfigSecrets": Object {},
"actionTypeId": "my-action-type",
"description": "my description",
},
Object {},
]
`);
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"my-action",
]
`);
Array [
"action",
"my-action",
]
`);
});

test('validates actionTypeConfig', async () => {
Expand All @@ -340,11 +339,9 @@ Array [
name: 'My action type',
unencryptedAttributes: [],
validate: {
config: Joi.object()
.keys({
param1: Joi.string().required(),
})
.required(),
config: schema.object({
param1: schema.string(),
}),
},
async executor() {},
});
Expand All @@ -366,7 +363,7 @@ Array [
options: {},
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"The following actionTypeConfig attributes are invalid: param1 [any.required]"`
`"The actionTypeConfig is invalid: [param1]: expected value of type [string] but got [undefined]"`
);
});

Expand Down Expand Up @@ -412,22 +409,22 @@ Array [
expect(result).toEqual(expectedResult);
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"my-action",
Object {
"actionTypeConfig": Object {
"a": true,
"c": true,
},
"actionTypeConfigSecrets": Object {
"b": true,
},
"actionTypeId": "my-action-type",
"description": "my description",
},
Object {},
]
`);
Array [
"action",
"my-action",
Object {
"actionTypeConfig": Object {
"a": true,
"c": true,
},
"actionTypeConfigSecrets": Object {
"b": true,
},
"actionTypeId": "my-action-type",
"description": "my description",
},
Object {},
]
`);
});
});
Loading

0 comments on commit ec8ec82

Please sign in to comment.