Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[ResponseOps] add pre-create, pre-update, and post-delete hooks for connectors #194081

Merged
merged 10 commits into from
Oct 9, 2024
72 changes: 70 additions & 2 deletions x-pack/plugins/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,16 @@ 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 |
| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number |
| maxAttempts | The maximum number of times this action will attempt to run when scheduled. | number |
| minimumLicenseRequired | The license required to use the action type. | string |
| supportedFeatureIds | List of IDs of the features that this action type is available in. Allowed values are `alerting`, `siem`, `uptime`, `cases`. See `x-pack/plugins/actions/common/connector_feature_config.ts` for the most up to date list. | string[] |
| 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 may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function |
| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function |
| executor | This is where the code of an action type lives. This is a function gets called for generating an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
| executor | This is where the code of an action type lives. This is a function gets called for generating an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function |
| preSaveHook | This optional function is called before the connector saved object is saved. For full details, see hooks section below. | Function |
| postSaveHook | This optional function is called after the connector saved object is saved. For full details, see hooks section below. | Function |
| postDeleteHook | This optional function is called after the connector saved object is deleted. For full details, see hooks section below. | Function |
| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | 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.
Expand All @@ -116,6 +119,71 @@ This is the primary function for an action type. Whenever the action needs to ru
| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) |

### Hooks

Hooks allow a connector implementation to be called during connector creation, update, and delete. When not using hooks, the connector implementation is not involved in creation, update and delete, except for the schema validation that happens for creation and update. Hooks can be used to force a create or update to fail, or run arbitrary code before and after update and create, and after delete. We don't have a need for a hook before delete at the moment, so that hook is currently not available.

Hooks are passed the following parameters:

```ts
interface PreSaveConnectorHookParams<Config, Secrets> {
connectorId: string;
config: Config;
secrets: Secrets;
logger: Logger;
request: KibanaRequest;
services: HookServices;
isUpdate: boolean;
}

interface PostSaveConnectorHookParams<Config, Secrets> {
connectorId: string;
config: Config;
secrets: Secrets;
logger: Logger;
request: KibanaRequest;
services: HookServices;
isUpdate: boolean;
wasSuccessful: boolean;
}

interface PostDeleteConnectorHookParams<Config, Secrets> {
connectorId: string;
config: Config;
// secrets not provided, yet
logger: Logger;
request: KibanaRequest;
services: HookServices;
}
```

| parameter | description
| --------- | -----------
| `connectorId` | The id of the connector.
| `config` | The connector's `config` object.
| `secrets` | The connector's `secrets` object.
| `logger` | A standard Kibana logger.
| `request` | The request causing this operation
| `services` | Common service objects, see below.
| `isUpdate` | For the `PreSave` and `PostSave` hooks, `isUpdate` is false for create operations, and true for update operations.
| `wasSuccessful` | For the `PostSave` hook, this indicates if the connector was persisted as a Saved Object successfully.

The `services` object contains the following properties:

| property | description
| --------- | -----------
| `scopedClusterClient` | A standard `scopeClusterClient` object.

The hooks are called just before, and just after, the Saved Object operation for the client methods is invoked.

The `PostDelete` hook does not have a `wasSuccessful` property, as the hook is not called if the delete operation fails. The saved object will still exist. Only a successful call to delete the connector will cause the hook to run.

The `PostSave` hook is useful if the `PreSave` hook is creating / modifying other resources. The `PreSave` hook is called just before the connector SO is actually created/updated, and of course that create/update could fail for some reason. In those cases, the `PostSave` hook is passed `wasSuccessful: false` and can "undo" any work it did in the `PreSave` hook.

The `PreSave` hook can be used to cancel a create or update, by throwing an exception. The `PostSave` and `PostDelete` invocations will have thrown exceptions caught and logged to the Kibana log, and will not cancel the operation.

When throwing an error in the `PreSave` hook, the Error's message will be used as the error failing the operation, so should include a human-readable description of what it was doing, along with any message from an underlying API that failed, if available. When an error is thrown from a `PreSave` hook, the `PostSave` hook will **NOT** be run.

### Example

The built-in email action type provides a good example of creating an action type with non-trivial configuration and params:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ const mockTaskManager = taskManagerMock.createSetup();
const configurationUtilities = actionsConfigMock.create();
const eventLogClient = eventLogClientMock.create();
const getEventLogClient = jest.fn();
const preSaveHook = jest.fn();
const postSaveHook = jest.fn();
const postDeleteHook = jest.fn();

let actionsClient: ActionsClient;
let mockedLicenseState: jest.Mocked<ILicenseState>;
Expand Down Expand Up @@ -392,6 +395,8 @@ describe('create()', () => {
params: { schema: schema.object({}) },
},
executor,
preSaveHook,
postSaveHook,
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
const result = await actionsClient.create({
Expand Down Expand Up @@ -428,6 +433,8 @@ describe('create()', () => {
},
]
`);
expect(preSaveHook).toHaveBeenCalledTimes(1);
expect(postSaveHook).toHaveBeenCalledTimes(1);
});

test('validates config', async () => {
Expand Down Expand Up @@ -1973,6 +1980,33 @@ describe('getOAuthAccessToken()', () => {
});

describe('delete()', () => {
beforeEach(() => {
actionTypeRegistry.register({
id: 'my-action-delete',
name: 'My action type',
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
validate: {
config: { schema: schema.object({}) },
secrets: { schema: schema.object({}) },
params: { schema: schema.object({}) },
},
executor,
postDeleteHook: async (options) => postDeleteHook(options),
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'action',
attributes: {
actionTypeId: 'my-action-delete',
isMissingSecrets: false,
config: {},
secrets: {},
},
references: [],
});
});

describe('authorization', () => {
test('ensures user is authorised to delete actions', async () => {
await actionsClient.delete({ id: '1' });
Expand Down Expand Up @@ -2052,6 +2086,16 @@ describe('delete()', () => {
`);
});

test('calls postDeleteHook', async () => {
const expectedResult = Symbol();
unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult);

const result = await actionsClient.delete({ id: '1' });
expect(result).toEqual(expectedResult);
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
expect(postDeleteHook).toHaveBeenCalledTimes(1);
});

it('throws when trying to delete a preconfigured connector', async () => {
actionsClient = new ActionsClient({
logger,
Expand Down Expand Up @@ -2250,6 +2294,8 @@ describe('update()', () => {
params: { schema: schema.object({}) },
},
executor,
preSaveHook,
postSaveHook,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
Expand Down Expand Up @@ -2315,6 +2361,9 @@ describe('update()', () => {
"my-action",
]
`);

expect(preSaveHook).toHaveBeenCalledTimes(1);
expect(postSaveHook).toHaveBeenCalledTimes(1);
});

test('updates an action with isMissingSecrets "true" (set true as the import result), to isMissingSecrets', async () => {
Expand Down
Loading