Create REST API to fire actions (#39463)
* Create REST API to fire actions * Add more tests to the fire API * Remove dead file reference * Fix broken tests * Fix broken test * Update docs * Apply PR feedback
This commit is contained in:
parent
8680dfe478
commit
e522e757ba
|
@ -122,7 +122,7 @@ Params:
|
|||
|---|---|---|
|
||||
|id|The id of the action you're trying to get.|string|
|
||||
|
||||
#### `GET /api/action/types` List action types
|
||||
#### `GET /api/action/types`: List action types
|
||||
|
||||
No parameters.
|
||||
|
||||
|
@ -143,6 +143,20 @@ Payload:
|
|||
|references|An array of `name`, `type` and `id`. This is the same as `references` in the saved objects API. See the saved objects API documentation.<br><br>In most cases, you can leave this empty.|Array|
|
||||
|version|The document version when read|string|
|
||||
|
||||
#### `POST /api/action/{id}/_fire`: Fire action
|
||||
|
||||
Params:
|
||||
|
||||
|Property|Description|Type|
|
||||
|---|---|---|
|
||||
|id|The id of the action you're trying to fire.|string|
|
||||
|
||||
Payload:
|
||||
|
||||
|Property|Description|Type|
|
||||
|---|---|---|
|
||||
|params|The parameters the action type requires for the execution.|object|
|
||||
|
||||
## Firing actions
|
||||
|
||||
The plugin exposes a fire function that you can use to fire actions.
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
|
||||
type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;
|
||||
import { ActionTypeRegistryContract } from './types';
|
||||
|
||||
const createActionTypeRegistryMock = () => {
|
||||
const mocked: jest.Mocked<ActionTypeRegistryContract> = {
|
||||
|
|
|
@ -57,13 +57,7 @@ Array [
|
|||
`);
|
||||
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
|
||||
const call = getCreateTaskRunnerFunction.mock.calls[0][0];
|
||||
expect(call.actionType).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"executor": [MockFunction],
|
||||
"id": "my-action-type",
|
||||
"name": "My action type",
|
||||
}
|
||||
`);
|
||||
expect(call.actionTypeRegistry).toBeTruthy();
|
||||
expect(call.encryptedSavedObjectsPlugin).toBeTruthy();
|
||||
expect(call.getServices).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -6,19 +6,20 @@
|
|||
|
||||
import Boom from 'boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionType, Services } from './types';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { ActionType, GetServicesFunction } from './types';
|
||||
import { TaskManager, TaskRunCreatorFunction } from '../../task_manager';
|
||||
import { getCreateTaskRunnerFunction } from './lib';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
|
||||
|
||||
interface ConstructorOptions {
|
||||
getServices: (basePath: string) => Services;
|
||||
taskManager: TaskManager;
|
||||
getServices: GetServicesFunction;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
}
|
||||
|
||||
export class ActionTypeRegistry {
|
||||
private readonly getServices: (basePath: string) => Services;
|
||||
private readonly taskRunCreatorFunction: TaskRunCreatorFunction;
|
||||
private readonly getServices: GetServicesFunction;
|
||||
private readonly taskManager: TaskManager;
|
||||
private readonly actionTypes: Map<string, ActionType> = new Map();
|
||||
private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
|
@ -27,6 +28,11 @@ export class ActionTypeRegistry {
|
|||
this.getServices = getServices;
|
||||
this.taskManager = taskManager;
|
||||
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
|
||||
this.taskRunCreatorFunction = getCreateTaskRunnerFunction({
|
||||
actionTypeRegistry: this,
|
||||
getServices: this.getServices,
|
||||
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,11 +61,7 @@ export class ActionTypeRegistry {
|
|||
[`actions:${actionType.id}`]: {
|
||||
title: actionType.name,
|
||||
type: `actions:${actionType.id}`,
|
||||
createTaskRunner: getCreateTaskRunnerFunction({
|
||||
actionType,
|
||||
getServices: this.getServices,
|
||||
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
|
||||
}),
|
||||
createTaskRunner: this.taskRunCreatorFunction,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
getRoute,
|
||||
updateRoute,
|
||||
listActionTypesRoute,
|
||||
fireRoute,
|
||||
} from './routes';
|
||||
|
||||
import { registerBuiltInActionTypes } from './builtin_action_types';
|
||||
|
@ -33,7 +34,7 @@ export function init(server: Legacy.Server) {
|
|||
attributesToExcludeFromAAD: new Set(['description']),
|
||||
});
|
||||
|
||||
function getServices(basePath: string): Services {
|
||||
function getServices(basePath: string, overwrites: Partial<Services> = {}): Services {
|
||||
// Fake request is here to allow creating a scoped saved objects client
|
||||
// and use it when security is disabled. This will be replaced when the
|
||||
// future phase of API tokens is complete.
|
||||
|
@ -45,6 +46,7 @@ export function init(server: Legacy.Server) {
|
|||
log: server.log,
|
||||
callCluster: callWithInternalUser,
|
||||
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(fakeRequest),
|
||||
...overwrites,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -64,6 +66,11 @@ export function init(server: Legacy.Server) {
|
|||
findRoute(server);
|
||||
updateRoute(server);
|
||||
listActionTypesRoute(server);
|
||||
fireRoute({
|
||||
server,
|
||||
actionTypeRegistry,
|
||||
getServices,
|
||||
});
|
||||
|
||||
const fireFn = createFireFunction({
|
||||
taskManager: taskManager!,
|
||||
|
|
163
x-pack/legacy/plugins/actions/server/lib/execute.test.ts
Normal file
163
x-pack/legacy/plugins/actions/server/lib/execute.test.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { execute } from './execute';
|
||||
import { actionTypeRegistryMock } from '../action_type_registry.mock';
|
||||
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
|
||||
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
|
||||
function getServices() {
|
||||
return {
|
||||
savedObjectsClient,
|
||||
log: jest.fn(),
|
||||
callCluster: jest.fn(),
|
||||
};
|
||||
}
|
||||
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
|
||||
const executeParams = {
|
||||
actionId: '1',
|
||||
namespace: 'some-namespace',
|
||||
services: getServices(),
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsPlugin,
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
test('successfully executes', async () => {
|
||||
const actionType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const actionSavedObject = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
actionTypeConfig: {
|
||||
bar: true,
|
||||
},
|
||||
actionTypeConfigSecrets: {
|
||||
baz: true,
|
||||
},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
await execute(executeParams);
|
||||
|
||||
expect(encryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith(
|
||||
'action',
|
||||
'1',
|
||||
{ namespace: 'some-namespace' }
|
||||
);
|
||||
|
||||
expect(actionTypeRegistry.get).toHaveBeenCalledWith('test');
|
||||
|
||||
expect(actionType.executor).toHaveBeenCalledWith({
|
||||
services: expect.anything(),
|
||||
config: {
|
||||
bar: true,
|
||||
baz: true,
|
||||
},
|
||||
params: { foo: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('provides empty config when actionTypeConfig and / or actionTypeConfigSecrets is empty', async () => {
|
||||
const actionType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const actionSavedObject = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
await execute(executeParams);
|
||||
|
||||
expect(actionType.executor).toHaveBeenCalledTimes(1);
|
||||
const executorCall = actionType.executor.mock.calls[0][0];
|
||||
expect(executorCall.config).toMatchInlineSnapshot(`Object {}`);
|
||||
});
|
||||
|
||||
test('throws an error when config is invalid', async () => {
|
||||
const actionType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
validate: {
|
||||
config: Joi.object()
|
||||
.keys({
|
||||
param1: Joi.string().required(),
|
||||
})
|
||||
.required(),
|
||||
},
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const actionSavedObject = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
|
||||
await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"The following actionTypeConfig attributes are invalid: param1 [any.required]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws an error when params is invalid', async () => {
|
||||
const actionType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
param1: Joi.string().required(),
|
||||
})
|
||||
.required(),
|
||||
},
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const actionSavedObject = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
|
||||
await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"params invalid: child \\"param1\\" fails because [\\"param1\\" is required]"`
|
||||
);
|
||||
});
|
45
x-pack/legacy/plugins/actions/server/lib/execute.ts
Normal file
45
x-pack/legacy/plugins/actions/server/lib/execute.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Services, ActionTypeRegistryContract } from '../types';
|
||||
import { validateActionTypeConfig } from './validate_action_type_config';
|
||||
import { validateActionTypeParams } from './validate_action_type_params';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
|
||||
|
||||
interface ExecuteOptions {
|
||||
actionId: string;
|
||||
namespace: string;
|
||||
services: Services;
|
||||
params: Record<string, any>;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
}
|
||||
|
||||
export async function execute({
|
||||
actionId,
|
||||
namespace,
|
||||
actionTypeRegistry,
|
||||
services,
|
||||
params,
|
||||
encryptedSavedObjectsPlugin,
|
||||
}: ExecuteOptions) {
|
||||
// TODO: Ensure user can read the action before processing
|
||||
const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', actionId, {
|
||||
namespace,
|
||||
});
|
||||
const actionType = actionTypeRegistry.get(action.attributes.actionTypeId);
|
||||
const mergedActionTypeConfig = {
|
||||
...(action.attributes.actionTypeConfig || {}),
|
||||
...(action.attributes.actionTypeConfigSecrets || {}),
|
||||
};
|
||||
const validatedConfig = validateActionTypeConfig(actionType, mergedActionTypeConfig);
|
||||
const validatedParams = validateActionTypeParams(actionType, params);
|
||||
await actionType.executor({
|
||||
services,
|
||||
config: validatedConfig,
|
||||
params: validatedParams,
|
||||
});
|
||||
}
|
|
@ -4,13 +4,26 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
jest.mock('./execute', () => ({
|
||||
execute: jest.fn(),
|
||||
}));
|
||||
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
|
||||
import { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
|
||||
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
import { actionTypeRegistryMock } from '../action_type_registry.mock';
|
||||
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
||||
|
||||
const actionType = {
|
||||
id: '1',
|
||||
name: '1',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
|
||||
actionTypeRegistry.get.mockReturnValue(actionType);
|
||||
|
||||
const getCreateTaskRunnerFunctionParams = {
|
||||
getServices() {
|
||||
return {
|
||||
|
@ -19,11 +32,7 @@ const getCreateTaskRunnerFunctionParams = {
|
|||
savedObjectsClient: SavedObjectsClientMock.create(),
|
||||
};
|
||||
},
|
||||
actionType: {
|
||||
id: '1',
|
||||
name: '1',
|
||||
executor: jest.fn(),
|
||||
},
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin,
|
||||
};
|
||||
|
||||
|
@ -40,101 +49,19 @@ const taskInstanceMock = {
|
|||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
test('successfully executes the task', async () => {
|
||||
test('executes the task by calling the executor with proper parameters', async () => {
|
||||
const { execute: mockExecute } = jest.requireMock('./execute');
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
references: [],
|
||||
attributes: {
|
||||
actionTypeConfig: { foo: true },
|
||||
actionTypeConfigSecrets: { bar: true },
|
||||
},
|
||||
});
|
||||
const runnerResult = await runner.run();
|
||||
|
||||
expect(runnerResult).toBeUndefined();
|
||||
expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mock.calls[0])
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"2",
|
||||
Object {
|
||||
"namespace": "test",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(getCreateTaskRunnerFunctionParams.actionType.executor).toHaveBeenCalledTimes(1);
|
||||
const call = getCreateTaskRunnerFunctionParams.actionType.executor.mock.calls[0][0];
|
||||
expect(call.config).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bar": true,
|
||||
"foo": true,
|
||||
}
|
||||
`);
|
||||
expect(call.params).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"baz": true,
|
||||
}
|
||||
`);
|
||||
expect(call.services).toBeTruthy();
|
||||
});
|
||||
|
||||
test('validates params before executing the task', async () => {
|
||||
const createTaskRunner = getCreateTaskRunnerFunction({
|
||||
...getCreateTaskRunnerFunctionParams,
|
||||
actionType: {
|
||||
...getCreateTaskRunnerFunctionParams.actionType,
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
param1: Joi.string().required(),
|
||||
})
|
||||
.required(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
references: [],
|
||||
attributes: {
|
||||
actionTypeConfig: { foo: true },
|
||||
actionTypeConfigSecrets: { bar: true },
|
||||
},
|
||||
});
|
||||
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"params invalid: child \\"param1\\" fails because [\\"param1\\" is required]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('validates config before executing the task', async () => {
|
||||
const createTaskRunner = getCreateTaskRunnerFunction({
|
||||
...getCreateTaskRunnerFunctionParams,
|
||||
actionType: {
|
||||
...getCreateTaskRunnerFunctionParams.actionType,
|
||||
validate: {
|
||||
config: Joi.object()
|
||||
.keys({
|
||||
param1: Joi.string().required(),
|
||||
})
|
||||
.required(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'action',
|
||||
references: [],
|
||||
attributes: {
|
||||
actionTypeConfig: { foo: true },
|
||||
actionTypeConfigSecrets: { bar: true },
|
||||
},
|
||||
});
|
||||
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"The following actionTypeConfig attributes are invalid: param1 [any.required]"`
|
||||
);
|
||||
expect(mockExecute).toHaveBeenCalledWith({
|
||||
namespace: 'test',
|
||||
actionId: '2',
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin,
|
||||
services: expect.anything(),
|
||||
params: { baz: true },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,15 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ActionType, Services } from '../types';
|
||||
import { execute } from './execute';
|
||||
import { ActionTypeRegistryContract, GetServicesFunction } from '../types';
|
||||
import { TaskInstance } from '../../../task_manager';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
|
||||
import { validateActionTypeConfig } from './validate_action_type_config';
|
||||
import { validateActionTypeParams } from './validate_action_type_params';
|
||||
|
||||
interface CreateTaskRunnerFunctionOptions {
|
||||
getServices: (basePath: string) => Services;
|
||||
actionType: ActionType;
|
||||
getServices: GetServicesFunction;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
}
|
||||
|
||||
|
@ -22,29 +21,20 @@ interface TaskRunnerOptions {
|
|||
|
||||
export function getCreateTaskRunnerFunction({
|
||||
getServices,
|
||||
actionType,
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsPlugin,
|
||||
}: CreateTaskRunnerFunctionOptions) {
|
||||
return ({ taskInstance }: TaskRunnerOptions) => {
|
||||
return {
|
||||
run: async () => {
|
||||
const { namespace, id, actionTypeParams } = taskInstance.params;
|
||||
const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', id, {
|
||||
await execute({
|
||||
namespace,
|
||||
});
|
||||
const mergedActionTypeConfig = {
|
||||
...(action.attributes.actionTypeConfig || {}),
|
||||
...(action.attributes.actionTypeConfigSecrets || {}),
|
||||
};
|
||||
const validatedActionTypeConfig = validateActionTypeConfig(
|
||||
actionType,
|
||||
mergedActionTypeConfig
|
||||
);
|
||||
const validatedActionTypeParams = validateActionTypeParams(actionType, actionTypeParams);
|
||||
await actionType.executor({
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsPlugin,
|
||||
actionId: id,
|
||||
services: getServices(taskInstance.params.basePath),
|
||||
config: validatedActionTypeConfig,
|
||||
params: validatedActionTypeParams,
|
||||
params: actionTypeParams,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { execute } from './execute';
|
||||
export { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
|
||||
export { validateActionTypeConfig } from './validate_action_type_config';
|
||||
export { validateActionTypeParams } from './validate_action_type_params';
|
||||
|
|
|
@ -8,6 +8,7 @@ import Hapi from 'hapi';
|
|||
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
import { actionsClientMock } from '../actions_client.mock';
|
||||
import { actionTypeRegistryMock } from '../action_type_registry.mock';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
|
||||
|
||||
const defaultConfig = {
|
||||
'kibana.index': '.kibana',
|
||||
|
@ -21,6 +22,7 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
|
|||
const actionsClient = actionsClientMock.create();
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.create();
|
||||
|
||||
server.config = () => {
|
||||
return {
|
||||
|
@ -41,8 +43,21 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
|
|||
},
|
||||
});
|
||||
|
||||
server.register({
|
||||
name: 'encrypted_saved_objects',
|
||||
register(pluginServer: Hapi.Server) {
|
||||
pluginServer.expose('isEncryptionError', encryptedSavedObjects.isEncryptionError);
|
||||
pluginServer.expose('registerType', encryptedSavedObjects.registerType);
|
||||
pluginServer.expose(
|
||||
'getDecryptedAsInternalUser',
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient);
|
||||
server.decorate('request', 'getActionsClient', () => actionsClient);
|
||||
server.decorate('request', 'getBasePath', () => '/s/my-space');
|
||||
|
||||
return { server, savedObjectsClient, actionsClient, actionTypeRegistry };
|
||||
}
|
||||
|
|
62
x-pack/legacy/plugins/actions/server/routes/fire.test.ts
Normal file
62
x-pack/legacy/plugins/actions/server/routes/fire.test.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
jest.mock('../lib/execute', () => ({
|
||||
execute: jest.fn(),
|
||||
}));
|
||||
|
||||
import { createMockServer } from './_mock_server';
|
||||
import { fireRoute } from './fire';
|
||||
|
||||
const getServices = jest.fn();
|
||||
|
||||
const { server, actionTypeRegistry, savedObjectsClient } = createMockServer();
|
||||
fireRoute({ server, actionTypeRegistry, getServices });
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
it('fires an action with proper parameters', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { execute } = require('../lib/execute');
|
||||
|
||||
const request = {
|
||||
method: 'POST',
|
||||
url: '/api/action/1/_fire',
|
||||
payload: {
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
getServices.mockReturnValue({
|
||||
log: jest.fn(),
|
||||
callCluster: jest.fn(),
|
||||
savedObjectsClient: jest.fn(),
|
||||
});
|
||||
execute.mockResolvedValueOnce({ success: true });
|
||||
|
||||
const { payload, statusCode } = await server.inject(request);
|
||||
expect(statusCode).toBe(204);
|
||||
expect(payload).toBe('');
|
||||
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
expect(execute).toHaveBeenCalledTimes(1);
|
||||
const executeCall = execute.mock.calls[0][0];
|
||||
expect(executeCall.params).toEqual({
|
||||
foo: true,
|
||||
});
|
||||
expect(executeCall.actionTypeRegistry).toBeTruthy();
|
||||
expect(executeCall.actionId).toBe('1');
|
||||
expect(executeCall.namespace).toBeUndefined();
|
||||
expect(executeCall.services).toBeTruthy();
|
||||
expect(executeCall.encryptedSavedObjectsPlugin).toBeTruthy();
|
||||
});
|
69
x-pack/legacy/plugins/actions/server/routes/fire.ts
Normal file
69
x-pack/legacy/plugins/actions/server/routes/fire.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import Hapi from 'hapi';
|
||||
import { execute } from '../lib';
|
||||
import { ActionTypeRegistryContract, GetServicesFunction } from '../types';
|
||||
|
||||
interface FireRequest extends Hapi.Request {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
payload: {
|
||||
params: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
interface FireRouteOptions {
|
||||
server: Hapi.Server;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
getServices: GetServicesFunction;
|
||||
}
|
||||
|
||||
export function fireRoute({ server, actionTypeRegistry, getServices }: FireRouteOptions) {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/action/{id}/_fire',
|
||||
options: {
|
||||
response: {
|
||||
emptyStatusCode: 204,
|
||||
},
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
.required(),
|
||||
payload: Joi.object()
|
||||
.keys({
|
||||
params: Joi.object().required(),
|
||||
})
|
||||
.required(),
|
||||
},
|
||||
},
|
||||
async handler(request: FireRequest, h: Hapi.ResponseToolkit) {
|
||||
const { id } = request.params;
|
||||
const { params } = request.payload;
|
||||
const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request);
|
||||
const savedObjectsClient = request.getSavedObjectsClient();
|
||||
// Ensure user can read the action and has access to it
|
||||
await savedObjectsClient.get('action', id);
|
||||
await execute({
|
||||
params,
|
||||
actionTypeRegistry,
|
||||
actionId: id,
|
||||
namespace: namespace === 'default' ? undefined : namespace,
|
||||
services: getServices(request.getBasePath(), { savedObjectsClient }),
|
||||
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!,
|
||||
});
|
||||
return h.response();
|
||||
},
|
||||
});
|
||||
}
|
|
@ -10,3 +10,4 @@ export { findRoute } from './find';
|
|||
export { getRoute } from './get';
|
||||
export { updateRoute } from './update';
|
||||
export { listActionTypesRoute } from './list_action_types';
|
||||
export { fireRoute } from './fire';
|
||||
|
|
|
@ -8,6 +8,8 @@ import { SavedObjectsClientContract } from 'src/core/server';
|
|||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (basePath: string, overwrites?: Partial<Services>) => Services;
|
||||
export type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;
|
||||
|
||||
export interface SavedObjectReference {
|
||||
name: string;
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
*/
|
||||
|
||||
export { TaskManager } from './types';
|
||||
export { TaskInstance, ConcreteTaskInstance } from './task';
|
||||
export { TaskInstance, ConcreteTaskInstance, TaskRunCreatorFunction } from './task';
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
|
||||
|
||||
export const ES_ARCHIVER_ACTION_ID = '19cfba7c-711a-4170-8590-9a99a281e85c';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -18,11 +17,12 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
|
||||
const esTestIndexName = '.kibaka-alerting-test-data';
|
||||
|
||||
describe('actions', () => {
|
||||
describe('fire', () => {
|
||||
beforeEach(() => esArchiver.load('actions/basic'));
|
||||
afterEach(() => esArchiver.unload('actions/basic'));
|
||||
|
||||
before(async () => {
|
||||
await es.indices.delete({ index: esTestIndexName, ignore: [404] });
|
||||
await es.indices.create({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
|
@ -53,9 +53,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
});
|
||||
after(() => es.indices.delete({ index: esTestIndexName }));
|
||||
|
||||
it('decrypts attributes and joins on actionTypeConfig when firing', async () => {
|
||||
it('decrypts attributes and joins on actionTypeConfig when calling fire API', async () => {
|
||||
await supertest
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/fire`)
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
|
@ -64,11 +64,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
message: 'Testing 123',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.expect(204)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
});
|
||||
expect(resp.body).to.eql({});
|
||||
});
|
||||
const indexedRecord = await retry.tryForTime(5000, async () => {
|
||||
const searchResult = await es.search({
|
||||
|
@ -110,7 +108,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
});
|
||||
});
|
||||
|
||||
it('encrypted attributes still available after update', async () => {
|
||||
it('fire still works with encrypted attributes after updating an action', async () => {
|
||||
const { body: updatedAction } = await supertest
|
||||
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
|
@ -139,7 +137,7 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
},
|
||||
});
|
||||
await supertest
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/fire`)
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
|
@ -148,11 +146,9 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
message: 'Testing 123',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.expect(204)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
success: true,
|
||||
});
|
||||
expect(resp.body).to.eql({});
|
||||
});
|
||||
const indexedRecord = await retry.tryForTime(5000, async () => {
|
||||
const searchResult = await es.search({
|
||||
|
@ -193,5 +189,37 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) {
|
|||
source: 'action:test.index-record',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 404 when action doesn't exist`, async () => {
|
||||
const { body: response } = await supertest
|
||||
.post('/api/action/1/_fire')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { foo: true },
|
||||
})
|
||||
.expect(404);
|
||||
expect(response).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [action/1] not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when payload is empty and invalid', async () => {
|
||||
const { body: response } = await supertest
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_fire`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({})
|
||||
.expect(400);
|
||||
expect(response).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "params" fails because ["params" is required]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['params'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -17,5 +17,6 @@ export default function actionsTests({ loadTestFile }: KibanaFunctionalTestDefau
|
|||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/server_log'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/slack'));
|
||||
loadTestFile(require.resolve('./fire'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ export default function alertTests({ getService }: KibanaFunctionalTestDefaultPr
|
|||
const createdAlertIds: string[] = [];
|
||||
|
||||
before(async () => {
|
||||
await destroyEsTestIndex(es);
|
||||
({ name: esTestIndexName } = await setupEsTestIndex(es));
|
||||
await esArchiver.load('actions/basic');
|
||||
});
|
||||
|
|
|
@ -61,5 +61,5 @@ export async function setupEsTestIndex(es: any) {
|
|||
}
|
||||
|
||||
export async function destroyEsTestIndex(es: any) {
|
||||
await es.indices.delete({ index: esTestIndexName });
|
||||
await es.indices.delete({ index: esTestIndexName, ignore: [404] });
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ export default async function ({ readConfigFile }) {
|
|||
testFiles: [
|
||||
require.resolve('./test_suites/task_manager'),
|
||||
require.resolve('./test_suites/encrypted_saved_objects'),
|
||||
require.resolve('./test_suites/actions'),
|
||||
],
|
||||
services: {
|
||||
retry: kibanaFunctionalConfig.get('services.retry'),
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import { Legacy } from 'kibana';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function actionsPlugin(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: 'actions-test',
|
||||
require: ['actions'],
|
||||
init(server: Legacy.Server) {
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/action/{id}/fire',
|
||||
options: {
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
id: Joi.string().required(),
|
||||
})
|
||||
.required(),
|
||||
payload: Joi.object()
|
||||
.keys({
|
||||
params: Joi.object(),
|
||||
})
|
||||
.required(),
|
||||
},
|
||||
},
|
||||
async handler(request: any) {
|
||||
await request.server.plugins.actions.fire({
|
||||
id: request.params.id,
|
||||
params: request.payload.params,
|
||||
});
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "actions-test",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) {
|
||||
describe('actions', function actionsSuite() {
|
||||
this.tags('ciGroup2');
|
||||
loadTestFile(require.resolve('./actions'));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue