mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Add security support for alerts and actions (#41389)
* Initial work * Cleanup add createAPIKey fn pt1 * Change getServices function to take request as parameter * Use API key when executing alerts * Revert task manager using encrypted saved objects * Store fired actions within a saved object to encrypt API keys * Fix fireActionId * Cleanup code, fix type check error * Add a type for getScopedSavedObjectsClient * Fix getBasePath and spaceIdToNamespace functions * Add safety check for API key and action * Fix integration tests * Fix broken jest tests * Cleanup * Rename generatedApiKey to apiKeyValue * Ensure access to action record * Cleanup * Add unit tests * Fix variable conflict * Revert task manager specific code (no longer needed) * Remove fire terminology * Move tests to spaces and security folder * Use ES Archiver to remove spaces (empty_kibana) * Fix missing pieces * Convert action tests to run per user * Convert alerting tests to run per user * Fix type check issue * Fix failing test * Add callCluster and savedObjectsClient authorization tests * Make savedObjectsClient return 403 for authorization tests * Cleanup * Fix test failure * Common function to get data from test index * Create ObjectRemover * Cleanup * useApiKey now provided to functions instead of relying on condition of two strings * Fix typo * Make tests it(...) start with should * Rename useApiKey to isSecurityEnabled * Merge apiKeyId and apiKeyValue into one * Update docs * Use feature controls for list alert / action types API * Remove need to add ! in TypeScript for required plugins * Fix ESLint issue * Include actions and alertTypeParams into AAD and genereate new API key on update * Generate random id for API key name attribute * Include interval in AAD * Send pre-encoded string * Fix ExecutorError * Fix apiKey snapshot * Fix 'default' typo * De-compose apiKey * Refresh API key when enabling / disabling an alert * Add updatedBy * Make unauthorized APIs return 404
This commit is contained in:
parent
7d09b9dfa6
commit
5734f1c1be
113 changed files with 4698 additions and 3314 deletions
|
@ -53,7 +53,7 @@ This is the primary function for an action type. Whenever the action needs to ex
|
|||
|config|The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type.|
|
||||
|params|Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function.|
|
||||
|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.|
|
||||
|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>**NOTE**: This currently only works when security is disabled. A future PR will add support for enabling security using Elasticsearch API tokens.|
|
||||
|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)|
|
||||
|
||||
### Example
|
||||
|
@ -146,6 +146,7 @@ The following table describes the properties of the `options` object.
|
|||
|id|The id of the action you want to execute.|string|
|
||||
|params|The `params` value to give the action type executor.|object|
|
||||
|spaceId|The space id the action is within.|string|
|
||||
|apiKey|The Elasticsearch API key to use for context. (Note: only required and used when security is enabled).|string|
|
||||
|
||||
### Example
|
||||
|
||||
|
|
|
@ -15,5 +15,19 @@
|
|||
"type": "binary"
|
||||
}
|
||||
}
|
||||
},
|
||||
"action_task_params": {
|
||||
"properties": {
|
||||
"actionId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"params": {
|
||||
"enabled": false,
|
||||
"type": "object"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ function getServices() {
|
|||
}
|
||||
const actionTypeRegistryParams = {
|
||||
getServices,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: mockTaskManager,
|
||||
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(),
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
@ -64,11 +65,14 @@ describe('register()', () => {
|
|||
},
|
||||
]
|
||||
`);
|
||||
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
|
||||
const call = getCreateTaskRunnerFunction.mock.calls[0][0];
|
||||
expect(call.actionTypeRegistry).toBeTruthy();
|
||||
expect(call.encryptedSavedObjectsPlugin).toBeTruthy();
|
||||
expect(call.getServices).toBeTruthy();
|
||||
expect(getCreateTaskRunnerFunction).toHaveBeenCalledWith({
|
||||
actionTypeRegistry,
|
||||
isSecurityEnabled: true,
|
||||
encryptedSavedObjectsPlugin: actionTypeRegistryParams.encryptedSavedObjectsPlugin,
|
||||
getServices: actionTypeRegistryParams.getServices,
|
||||
getBasePath: actionTypeRegistryParams.getBasePath,
|
||||
spaceIdToNamespace: actionTypeRegistryParams.spaceIdToNamespace,
|
||||
});
|
||||
});
|
||||
|
||||
test('throws error if action type already registered', () => {
|
||||
|
@ -104,7 +108,6 @@ describe('register()', () => {
|
|||
expect(getRetry(0, new Error())).toEqual(false);
|
||||
expect(getRetry(0, new ExecutorError('my message', {}, true))).toEqual(true);
|
||||
expect(getRetry(0, new ExecutorError('my message', {}, false))).toEqual(false);
|
||||
expect(getRetry(0, new ExecutorError('my message', {}, null))).toEqual(false);
|
||||
expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false);
|
||||
expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime);
|
||||
});
|
||||
|
|
|
@ -6,18 +6,23 @@
|
|||
|
||||
import Boom from 'boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionType, GetServicesFunction } from './types';
|
||||
import { TaskManager, TaskRunCreatorFunction } from '../../task_manager';
|
||||
import { getCreateTaskRunnerFunction, ExecutorError } from './lib';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
|
||||
import { SpacesPlugin } from '../../spaces';
|
||||
import {
|
||||
ActionType,
|
||||
GetBasePathFunction,
|
||||
GetServicesFunction,
|
||||
SpaceIdToNamespaceFunction,
|
||||
} from './types';
|
||||
|
||||
interface ConstructorOptions {
|
||||
isSecurityEnabled: boolean;
|
||||
taskManager: TaskManager;
|
||||
getServices: GetServicesFunction;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
|
||||
getBasePath: SpacesPlugin['getBasePath'];
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
getBasePath: GetBasePathFunction;
|
||||
}
|
||||
|
||||
export class ActionTypeRegistry {
|
||||
|
@ -31,9 +36,11 @@ export class ActionTypeRegistry {
|
|||
encryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace,
|
||||
getBasePath,
|
||||
isSecurityEnabled,
|
||||
}: ConstructorOptions) {
|
||||
this.taskManager = taskManager;
|
||||
this.taskRunCreatorFunction = getCreateTaskRunnerFunction({
|
||||
isSecurityEnabled,
|
||||
getServices,
|
||||
actionTypeRegistry: this,
|
||||
encryptedSavedObjectsPlugin,
|
||||
|
|
|
@ -29,6 +29,7 @@ function getServices() {
|
|||
|
||||
const actionTypeRegistryParams = {
|
||||
getServices,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: mockTaskManager,
|
||||
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
|
|
@ -41,6 +41,7 @@ const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
|||
beforeAll(() => {
|
||||
actionTypeRegistry = new ActionTypeRegistry({
|
||||
getServices,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: taskManagerMock.create(),
|
||||
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
|
|
@ -38,6 +38,7 @@ const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
|||
beforeAll(() => {
|
||||
actionTypeRegistry = new ActionTypeRegistry({
|
||||
getServices,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: taskManagerMock.create(),
|
||||
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
|
|
@ -33,6 +33,7 @@ const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
|||
beforeAll(() => {
|
||||
actionTypeRegistry = new ActionTypeRegistry({
|
||||
getServices,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: taskManagerMock.create(),
|
||||
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
|
|
@ -50,6 +50,7 @@ async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise<an
|
|||
beforeAll(() => {
|
||||
actionTypeRegistry = new ActionTypeRegistry({
|
||||
getServices,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: taskManagerMock.create(),
|
||||
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
|
|
|
@ -10,16 +10,17 @@ import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
|||
|
||||
const mockTaskManager = taskManagerMock.create();
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
const spaceIdToNamespace = jest.fn();
|
||||
const getBasePath = jest.fn();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('execute()', () => {
|
||||
test('schedules the action with all given parameters', async () => {
|
||||
const executeFn = createExecuteFunction({
|
||||
getBasePath,
|
||||
isSecurityEnabled: true,
|
||||
taskManager: mockTaskManager,
|
||||
internalSavedObjectsRepository: savedObjectsClient,
|
||||
spaceIdToNamespace,
|
||||
getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient),
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
|
@ -29,41 +30,129 @@ describe('execute()', () => {
|
|||
},
|
||||
references: [],
|
||||
});
|
||||
spaceIdToNamespace.mockReturnValueOnce('namespace1');
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '234',
|
||||
type: 'action_task_params',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
await executeFn({
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
});
|
||||
expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1);
|
||||
expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"params": Object {
|
||||
"actionTaskParamsId": "234",
|
||||
"spaceId": "default",
|
||||
},
|
||||
"scope": Array [
|
||||
"actions",
|
||||
],
|
||||
"state": Object {},
|
||||
"taskType": "actions:mock-action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledWith('action', '123');
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', {
|
||||
actionId: '123',
|
||||
params: { baz: false },
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
});
|
||||
});
|
||||
|
||||
test('uses API key when provided', async () => {
|
||||
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
|
||||
const executeFn = createExecuteFunction({
|
||||
getBasePath,
|
||||
taskManager: mockTaskManager,
|
||||
getScopedSavedObjectsClient,
|
||||
isSecurityEnabled: true,
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'mock-action',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '234',
|
||||
type: 'action_task_params',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await executeFn({
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
});
|
||||
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`doesn't use API keys when not provided`, async () => {
|
||||
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
|
||||
const executeFn = createExecuteFunction({
|
||||
isSecurityEnabled: false,
|
||||
getBasePath,
|
||||
taskManager: mockTaskManager,
|
||||
getScopedSavedObjectsClient,
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '123',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'mock-action',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '234',
|
||||
type: 'action_task_params',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await executeFn({
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
});
|
||||
expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1);
|
||||
expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"params": Object {
|
||||
"id": "123",
|
||||
"params": Object {
|
||||
"baz": false,
|
||||
},
|
||||
"spaceId": "default",
|
||||
},
|
||||
"scope": Array [
|
||||
"actions",
|
||||
],
|
||||
"state": Object {},
|
||||
"taskType": "actions:mock-action",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"action",
|
||||
"123",
|
||||
Object {
|
||||
"namespace": "namespace1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(spaceIdToNamespace).toHaveBeenCalledWith('default');
|
||||
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {},
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error when isSecurityEnabled is true and key not passed in`, async () => {
|
||||
const executeFn = createExecuteFunction({
|
||||
getBasePath,
|
||||
taskManager: mockTaskManager,
|
||||
getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient),
|
||||
isSecurityEnabled: true,
|
||||
});
|
||||
await expect(
|
||||
executeFn({
|
||||
id: '123',
|
||||
params: { baz: false },
|
||||
spaceId: 'default',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"API key is required. The attribute \\"apiKey\\" is missing."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,34 +6,57 @@
|
|||
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { SpacesPlugin } from '../../spaces';
|
||||
import { GetBasePathFunction } from './types';
|
||||
|
||||
interface CreateExecuteFunctionOptions {
|
||||
isSecurityEnabled: boolean;
|
||||
taskManager: TaskManager;
|
||||
internalSavedObjectsRepository: SavedObjectsClientContract;
|
||||
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
|
||||
getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract;
|
||||
getBasePath: GetBasePathFunction;
|
||||
}
|
||||
|
||||
export interface ExecuteOptions {
|
||||
id: string;
|
||||
params: Record<string, any>;
|
||||
spaceId: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export function createExecuteFunction({
|
||||
getBasePath,
|
||||
taskManager,
|
||||
internalSavedObjectsRepository,
|
||||
spaceIdToNamespace,
|
||||
isSecurityEnabled,
|
||||
getScopedSavedObjectsClient,
|
||||
}: CreateExecuteFunctionOptions) {
|
||||
return async function execute({ id, params, spaceId }: ExecuteOptions) {
|
||||
const namespace = spaceIdToNamespace(spaceId);
|
||||
const actionSavedObject = await internalSavedObjectsRepository.get('action', id, { namespace });
|
||||
return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) {
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
|
||||
if (isSecurityEnabled && !apiKey) {
|
||||
throw new Error('API key is required. The attribute "apiKey" is missing.');
|
||||
} else if (isSecurityEnabled) {
|
||||
requestHeaders.authorization = `ApiKey ${apiKey}`;
|
||||
}
|
||||
|
||||
// Since we're using API keys and accessing elasticsearch can only be done
|
||||
// via a request, we're faking one with the proper authorization headers.
|
||||
const fakeRequest: any = {
|
||||
headers: requestHeaders,
|
||||
getBasePath: () => getBasePath(spaceId),
|
||||
};
|
||||
|
||||
const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest);
|
||||
const actionSavedObject = await savedObjectsClient.get('action', id);
|
||||
const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
|
||||
actionId: id,
|
||||
params,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
await taskManager.schedule({
|
||||
taskType: `actions:${actionSavedObject.attributes.actionTypeId}`,
|
||||
params: {
|
||||
id,
|
||||
spaceId,
|
||||
params,
|
||||
actionTaskParamsId: actionTaskParamsRecord.id,
|
||||
},
|
||||
state: {},
|
||||
scope: ['actions'],
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import { Legacy } from 'kibana';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { ActionsClient } from './actions_client';
|
||||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
import { createExecuteFunction } from './create_execute_function';
|
||||
import { ActionsPlugin, Services } from './types';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
|
||||
import {
|
||||
createRoute,
|
||||
deleteRoute,
|
||||
|
@ -22,8 +25,21 @@ import { registerBuiltInActionTypes } from './builtin_action_types';
|
|||
import { SpacesPlugin } from '../../spaces';
|
||||
import { createOptionalPlugin } from '../../../server/lib/optional_plugin';
|
||||
|
||||
export function init(server: Legacy.Server) {
|
||||
// Extend PluginProperties to indicate which plugins are guaranteed to exist
|
||||
// due to being marked as dependencies
|
||||
interface Plugins extends Hapi.PluginProperties {
|
||||
task_manager: TaskManager;
|
||||
encrypted_saved_objects: EncryptedSavedObjectsPlugin;
|
||||
}
|
||||
|
||||
interface Server extends Legacy.Server {
|
||||
plugins: Plugins;
|
||||
}
|
||||
|
||||
export function init(server: Server) {
|
||||
const config = server.config();
|
||||
const taskManager = server.plugins.task_manager;
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const spaces = createOptionalPlugin<SpacesPlugin>(
|
||||
config,
|
||||
'xpack.spaces',
|
||||
|
@ -31,50 +47,67 @@ export function init(server: Legacy.Server) {
|
|||
'spaces'
|
||||
);
|
||||
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const savedObjectsRepositoryWithInternalUser = server.savedObjects.getSavedObjectsRepository(
|
||||
callWithInternalUser
|
||||
);
|
||||
server.plugins.xpack_main.registerFeature({
|
||||
id: 'actions',
|
||||
name: 'Actions',
|
||||
app: ['actions', 'kibana'],
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['action', 'action_task_params'],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
api: ['actions-read', 'actions-all'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: ['action_task_params'],
|
||||
read: ['action'],
|
||||
},
|
||||
ui: [],
|
||||
api: ['actions-read'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Encrypted attributes
|
||||
// - `secrets` properties will be encrypted
|
||||
// - `config` will be included in AAD
|
||||
// - everything else excluded from AAD
|
||||
server.plugins.encrypted_saved_objects!.registerType({
|
||||
server.plugins.encrypted_saved_objects.registerType({
|
||||
type: 'action',
|
||||
attributesToEncrypt: new Set(['secrets']),
|
||||
attributesToExcludeFromAAD: new Set(['description']),
|
||||
});
|
||||
server.plugins.encrypted_saved_objects.registerType({
|
||||
type: 'action_task_params',
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
});
|
||||
|
||||
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.
|
||||
const fakeRequest: any = {
|
||||
headers: {},
|
||||
getBasePath: () => basePath,
|
||||
};
|
||||
function getServices(request: any): Services {
|
||||
return {
|
||||
log: server.log.bind(server),
|
||||
callCluster: callWithInternalUser,
|
||||
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(fakeRequest),
|
||||
...overwrites,
|
||||
log: (...args) => server.log(...args),
|
||||
callCluster: (...args) => callWithRequest(request, ...args),
|
||||
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(request),
|
||||
};
|
||||
}
|
||||
function getBasePath(spaceId?: string): string {
|
||||
return spaces.isEnabled && spaceId
|
||||
? spaces.getBasePath(spaceId)
|
||||
: ((server.config().get('server.basePath') || '') as string);
|
||||
}
|
||||
function spaceIdToNamespace(spaceId?: string): string | undefined {
|
||||
return spaces.isEnabled && spaceId ? spaces.spaceIdToNamespace(spaceId) : undefined;
|
||||
}
|
||||
|
||||
const taskManager = server.plugins.task_manager!;
|
||||
const actionTypeRegistry = new ActionTypeRegistry({
|
||||
getServices,
|
||||
taskManager: taskManager!,
|
||||
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!,
|
||||
getBasePath(...args) {
|
||||
return spaces.isEnabled
|
||||
? spaces.getBasePath(...args)
|
||||
: server.config().get('server.basePath');
|
||||
},
|
||||
spaceIdToNamespace(...args) {
|
||||
return spaces.isEnabled ? spaces.spaceIdToNamespace(...args) : undefined;
|
||||
},
|
||||
taskManager,
|
||||
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects,
|
||||
getBasePath,
|
||||
spaceIdToNamespace,
|
||||
isSecurityEnabled: config.get('xpack.security.enabled'),
|
||||
});
|
||||
|
||||
registerBuiltInActionTypes(actionTypeRegistry);
|
||||
|
@ -93,11 +126,10 @@ export function init(server: Legacy.Server) {
|
|||
});
|
||||
|
||||
const executeFn = createExecuteFunction({
|
||||
taskManager: taskManager!,
|
||||
internalSavedObjectsRepository: savedObjectsRepositoryWithInternalUser,
|
||||
spaceIdToNamespace(...args) {
|
||||
return spaces.isEnabled ? spaces.spaceIdToNamespace(...args) : undefined;
|
||||
},
|
||||
taskManager,
|
||||
getScopedSavedObjectsClient: server.savedObjects.getScopedSavedObjectsClient,
|
||||
getBasePath,
|
||||
isSecurityEnabled: config.get('xpack.security.enabled'),
|
||||
});
|
||||
|
||||
// Expose functions to server
|
||||
|
|
|
@ -166,3 +166,8 @@ test('throws an error when params is invalid', async () => {
|
|||
message: `error validating action params: [param1]: expected value of type [string] but got [undefined]`,
|
||||
});
|
||||
});
|
||||
|
||||
test('throws an error when failing to load action through savedObjectsClient', async () => {
|
||||
savedObjectsClient.get.mockRejectedValueOnce(new Error('No access'));
|
||||
await expect(execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot(`"No access"`);
|
||||
});
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Services, ActionTypeRegistryContract, ActionTypeExecutorResult } from '../types';
|
||||
import {
|
||||
ActionTypeExecutorResult,
|
||||
ActionTypeRegistryContract,
|
||||
RawAction,
|
||||
Services,
|
||||
} from '../types';
|
||||
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
|
||||
|
||||
|
@ -25,11 +30,18 @@ export async function execute({
|
|||
params,
|
||||
encryptedSavedObjectsPlugin,
|
||||
}: ExecuteOptions): Promise<ActionTypeExecutorResult> {
|
||||
// TODO: Ensure user can read the action before processing
|
||||
const action = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser('action', actionId, {
|
||||
// Ensure user can read the action before processing
|
||||
const {
|
||||
attributes: { actionTypeId, config, description },
|
||||
} = await services.savedObjectsClient.get<RawAction>('action', actionId);
|
||||
// Only get encrypted attributes here, the remaining attributes can be fetched in
|
||||
// the savedObjectsClient call
|
||||
const {
|
||||
attributes: { secrets },
|
||||
} = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAction>('action', actionId, {
|
||||
namespace,
|
||||
});
|
||||
const actionType = actionTypeRegistry.get(action.attributes.actionTypeId);
|
||||
const actionType = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
let validatedParams;
|
||||
let validatedConfig;
|
||||
|
@ -37,15 +49,13 @@ export async function execute({
|
|||
|
||||
try {
|
||||
validatedParams = validateParams(actionType, params);
|
||||
validatedConfig = validateConfig(actionType, action.attributes.config);
|
||||
validatedSecrets = validateSecrets(actionType, action.attributes.secrets);
|
||||
validatedConfig = validateConfig(actionType, config);
|
||||
validatedSecrets = validateSecrets(actionType, secrets);
|
||||
} catch (err) {
|
||||
return { status: 'error', message: err.message, retry: false };
|
||||
}
|
||||
|
||||
let result: ActionTypeExecutorResult | null = null;
|
||||
|
||||
const { actionTypeId, description } = action.attributes;
|
||||
const actionLabel = `${actionId} - ${actionTypeId} - ${description}`;
|
||||
|
||||
try {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
export class ExecutorError extends Error {
|
||||
readonly data?: any;
|
||||
readonly retry?: null | boolean | Date;
|
||||
constructor(message?: string, data?: any, retry?: null | boolean | Date) {
|
||||
readonly retry: boolean | Date;
|
||||
constructor(message?: string, data?: any, retry: boolean | Date = false) {
|
||||
super(message);
|
||||
this.data = data;
|
||||
this.retry = retry;
|
||||
|
|
|
@ -23,35 +23,37 @@ const actionType = {
|
|||
name: '1',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const services = {
|
||||
log: jest.fn(),
|
||||
callCluster: jest.fn(),
|
||||
savedObjectsClient: SavedObjectsClientMock.create(),
|
||||
};
|
||||
|
||||
actionTypeRegistry.get.mockReturnValue(actionType);
|
||||
|
||||
const getCreateTaskRunnerFunctionParams = {
|
||||
getServices() {
|
||||
return {
|
||||
log: jest.fn(),
|
||||
callCluster: jest.fn(),
|
||||
savedObjectsClient: SavedObjectsClientMock.create(),
|
||||
};
|
||||
},
|
||||
getServices: jest.fn().mockReturnValue(services),
|
||||
actionTypeRegistry,
|
||||
spaceIdToNamespace,
|
||||
encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin,
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
isSecurityEnabled: true,
|
||||
};
|
||||
|
||||
const taskInstanceMock = {
|
||||
runAt: new Date(),
|
||||
state: {},
|
||||
params: {
|
||||
id: '2',
|
||||
params: { baz: true },
|
||||
spaceId: 'test',
|
||||
actionTaskParamsId: '3',
|
||||
},
|
||||
taskType: 'actions:1',
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
getCreateTaskRunnerFunctionParams.getServices.mockReturnValue(services);
|
||||
});
|
||||
|
||||
test('executes the task by calling the executor with proper parameters', async () => {
|
||||
const { execute: mockExecute } = jest.requireMock('./execute');
|
||||
|
@ -60,11 +62,26 @@ test('executes the task by calling the executor with proper parameters', async (
|
|||
|
||||
mockExecute.mockResolvedValueOnce({ status: 'ok' });
|
||||
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '3',
|
||||
type: 'action_task_params',
|
||||
attributes: {
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
const runnerResult = await runner.run();
|
||||
|
||||
expect(runnerResult).toBeUndefined();
|
||||
expect(spaceIdToNamespace).toHaveBeenCalledWith('test');
|
||||
expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith(
|
||||
'action_task_params',
|
||||
'3',
|
||||
{ namespace: 'namespace-test' }
|
||||
);
|
||||
expect(mockExecute).toHaveBeenCalledWith({
|
||||
namespace: 'namespace-test',
|
||||
actionId: '2',
|
||||
|
@ -80,6 +97,16 @@ test('throws an error with suggested retry logic when return status is error', a
|
|||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '3',
|
||||
type: 'action_task_params',
|
||||
attributes: {
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
mockExecute.mockResolvedValueOnce({
|
||||
status: 'error',
|
||||
message: 'Error message',
|
||||
|
@ -96,3 +123,111 @@ test('throws an error with suggested retry logic when return status is error', a
|
|||
expect(e.retry).toEqual(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('uses API key when provided', async () => {
|
||||
const { execute: mockExecute } = jest.requireMock('./execute');
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
|
||||
mockExecute.mockResolvedValueOnce({ status: 'ok' });
|
||||
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '3',
|
||||
type: 'action_task_params',
|
||||
attributes: {
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await runner.run();
|
||||
|
||||
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`doesn't use API key when not provided`, async () => {
|
||||
const { execute: mockExecute } = jest.requireMock('./execute');
|
||||
const createTaskRunner = getCreateTaskRunnerFunction({
|
||||
...getCreateTaskRunnerFunctionParams,
|
||||
isSecurityEnabled: false,
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
|
||||
mockExecute.mockResolvedValueOnce({ status: 'ok' });
|
||||
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '3',
|
||||
type: 'action_task_params',
|
||||
attributes: {
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await runner.run();
|
||||
|
||||
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {},
|
||||
});
|
||||
});
|
||||
|
||||
test(`doesn't use API key when provided and isSecurityEnabled is set to false`, async () => {
|
||||
const { execute: mockExecute } = jest.requireMock('./execute');
|
||||
const createTaskRunner = getCreateTaskRunnerFunction({
|
||||
...getCreateTaskRunnerFunctionParams,
|
||||
isSecurityEnabled: false,
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
|
||||
mockExecute.mockResolvedValueOnce({ status: 'ok' });
|
||||
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '3',
|
||||
type: 'action_task_params',
|
||||
attributes: {
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await runner.run();
|
||||
|
||||
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {},
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error when isSecurityEnabled is true but key isn't provided`, async () => {
|
||||
const { execute: mockExecute } = jest.requireMock('./execute');
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
const runner = createTaskRunner({ taskInstance: taskInstanceMock });
|
||||
|
||||
mockExecute.mockResolvedValueOnce({ status: 'ok' });
|
||||
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
|
||||
mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '3',
|
||||
type: 'action_task_params',
|
||||
attributes: {
|
||||
actionId: '2',
|
||||
params: { baz: true },
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"API key is required. The attribute \\"apiKey\\" is missing."`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,17 +6,23 @@
|
|||
|
||||
import { execute } from './execute';
|
||||
import { ExecutorError } from './executor_error';
|
||||
import { ActionTypeRegistryContract, GetServicesFunction } from '../types';
|
||||
import { TaskInstance } from '../../../task_manager';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
|
||||
import { SpacesPlugin } from '../../../spaces';
|
||||
import {
|
||||
ActionTaskParams,
|
||||
ActionTypeRegistryContract,
|
||||
GetBasePathFunction,
|
||||
GetServicesFunction,
|
||||
SpaceIdToNamespaceFunction,
|
||||
} from '../types';
|
||||
|
||||
interface CreateTaskRunnerFunctionOptions {
|
||||
getServices: GetServicesFunction;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
|
||||
getBasePath: SpacesPlugin['getBasePath'];
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
getBasePath: GetBasePathFunction;
|
||||
isSecurityEnabled: boolean;
|
||||
}
|
||||
|
||||
interface TaskRunnerOptions {
|
||||
|
@ -29,19 +35,42 @@ export function getCreateTaskRunnerFunction({
|
|||
encryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace,
|
||||
getBasePath,
|
||||
isSecurityEnabled,
|
||||
}: CreateTaskRunnerFunctionOptions) {
|
||||
return ({ taskInstance }: TaskRunnerOptions) => {
|
||||
return {
|
||||
run: async () => {
|
||||
const { spaceId, id, params } = taskInstance.params;
|
||||
const { spaceId, actionTaskParamsId } = taskInstance.params;
|
||||
const namespace = spaceIdToNamespace(spaceId);
|
||||
const basePath = getBasePath(spaceId);
|
||||
|
||||
const {
|
||||
attributes: { actionId, params, apiKey },
|
||||
} = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<ActionTaskParams>(
|
||||
'action_task_params',
|
||||
actionTaskParamsId,
|
||||
{ namespace }
|
||||
);
|
||||
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
if (isSecurityEnabled && !apiKey) {
|
||||
throw new ExecutorError('API key is required. The attribute "apiKey" is missing.');
|
||||
} else if (isSecurityEnabled) {
|
||||
requestHeaders.authorization = `ApiKey ${apiKey}`;
|
||||
}
|
||||
|
||||
// Since we're using API keys and accessing elasticsearch can only be done
|
||||
// via a request, we're faking one with the proper authorization headers.
|
||||
const fakeRequest: any = {
|
||||
headers: requestHeaders,
|
||||
getBasePath: () => getBasePath(spaceId),
|
||||
};
|
||||
|
||||
const executorResult = await execute({
|
||||
namespace,
|
||||
actionTypeRegistry,
|
||||
encryptedSavedObjectsPlugin,
|
||||
actionId: id,
|
||||
services: getServices(basePath),
|
||||
actionId,
|
||||
services: getServices(fakeRequest),
|
||||
params,
|
||||
});
|
||||
if (executorResult.status === 'error') {
|
||||
|
@ -50,7 +79,7 @@ export function getCreateTaskRunnerFunction({
|
|||
throw new ExecutorError(
|
||||
executorResult.message,
|
||||
executorResult.data,
|
||||
executorResult.retry
|
||||
executorResult.retry == null ? false : executorResult.retry
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -28,6 +28,7 @@ export function createRoute(server: Hapi.Server) {
|
|||
method: 'POST',
|
||||
path: `/api/action`,
|
||||
options: {
|
||||
tags: ['access:actions-all'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
|
|
|
@ -18,6 +18,7 @@ export function deleteRoute(server: Hapi.Server) {
|
|||
method: 'DELETE',
|
||||
path: `/api/action/{id}`,
|
||||
options: {
|
||||
tags: ['access:actions-all'],
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
|
|
|
@ -13,7 +13,7 @@ import { executeRoute } from './execute';
|
|||
|
||||
const getServices = jest.fn();
|
||||
|
||||
const { server, actionTypeRegistry, savedObjectsClient } = createMockServer();
|
||||
const { server, actionTypeRegistry } = createMockServer();
|
||||
executeRoute({ server, actionTypeRegistry, getServices });
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
@ -42,13 +42,6 @@ it('executes an action with proper parameters', async () => {
|
|||
expect(statusCode).toBe(200);
|
||||
expect(payload).toBe('{"success":true}');
|
||||
|
||||
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({
|
||||
|
|
|
@ -29,6 +29,7 @@ export function executeRoute({ server, actionTypeRegistry, getServices }: Execut
|
|||
method: 'POST',
|
||||
path: '/api/action/{id}/_execute',
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
response: {
|
||||
emptyStatusCode: 204,
|
||||
},
|
||||
|
@ -52,15 +53,12 @@ export function executeRoute({ server, actionTypeRegistry, getServices }: Execut
|
|||
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);
|
||||
const result = await execute({
|
||||
params,
|
||||
actionTypeRegistry,
|
||||
actionId: id,
|
||||
namespace: namespace === 'default' ? undefined : namespace,
|
||||
services: getServices(request.getBasePath(), { savedObjectsClient }),
|
||||
services: getServices(request),
|
||||
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects!,
|
||||
});
|
||||
return result;
|
||||
|
|
|
@ -30,6 +30,7 @@ export function findRoute(server: Hapi.Server) {
|
|||
method: 'GET',
|
||||
path: `/api/action/_find`,
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
validate: {
|
||||
query: Joi.object()
|
||||
.keys({
|
||||
|
|
|
@ -18,6 +18,7 @@ export function getRoute(server: Hapi.Server) {
|
|||
method: 'GET',
|
||||
path: `/api/action/{id}`,
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
|
|
|
@ -10,6 +10,9 @@ export function listActionTypesRoute(server: Hapi.Server) {
|
|||
server.route({
|
||||
method: 'GET',
|
||||
path: `/api/action/types`,
|
||||
options: {
|
||||
tags: ['access:actions-read'],
|
||||
},
|
||||
async handler(request: Hapi.Request) {
|
||||
return request.server.plugins.actions!.listTypes();
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ export function updateRoute(server: Hapi.Server) {
|
|||
method: 'PUT',
|
||||
path: `/api/action/{id}`,
|
||||
options: {
|
||||
tags: ['access:actions-all'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
|
|
|
@ -4,13 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { SavedObjectsClientContract, SavedObjectAttributes } from 'src/core/server';
|
||||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
import { ExecuteOptions } from './create_execute_function';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (basePath: string, overwrites?: Partial<Services>) => Services;
|
||||
export type GetServicesFunction = (request: any) => Services;
|
||||
export type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>;
|
||||
export type GetBasePathFunction = (spaceId?: string) => string;
|
||||
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
|
||||
|
||||
export interface Services {
|
||||
callCluster(path: string, opts: any): Promise<any>;
|
||||
|
@ -68,3 +70,16 @@ export interface ActionType {
|
|||
};
|
||||
executor: ExecutorType;
|
||||
}
|
||||
|
||||
export interface RawAction extends SavedObjectAttributes {
|
||||
actionTypeId: string;
|
||||
description: string;
|
||||
config: SavedObjectAttributes;
|
||||
secrets: SavedObjectAttributes;
|
||||
}
|
||||
|
||||
export interface ActionTaskParams extends SavedObjectAttributes {
|
||||
actionId: string;
|
||||
params: Record<string, any>;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
|
|
@ -49,8 +49,8 @@ This is the primary function for an alert type. Whenever the alert needs to exec
|
|||
|
||||
|Property|Description|
|
||||
|---|---|
|
||||
|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.|
|
||||
|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>**NOTE**: This currently only works when security is disabled. A future PR will add support for enabled security using Elasticsearch API tokens.|
|
||||
|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but in the context of the user who created the alert when security is enabled.|
|
||||
|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 who created the alert (only when security isenabled).|
|
||||
|services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)|
|
||||
|startedAt|The date and time the alert type started execution.|
|
||||
|previousStartedAt|The previous date and time the alert type started a successful execution.|
|
||||
|
|
|
@ -15,7 +15,7 @@ export function alerting(kibana: any) {
|
|||
return new kibana.Plugin({
|
||||
id: 'alerting',
|
||||
configPrefix: 'xpack.alerting',
|
||||
require: ['kibana', 'elasticsearch', 'actions', 'task_manager'],
|
||||
require: ['kibana', 'elasticsearch', 'actions', 'task_manager', 'encrypted_saved_objects'],
|
||||
isEnabled(config: Legacy.KibanaConfig) {
|
||||
return (
|
||||
config.get('xpack.alerting.enabled') === true &&
|
||||
|
|
|
@ -31,6 +31,15 @@
|
|||
},
|
||||
"scheduledTaskId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updatedBy": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,12 @@ jest.mock('./lib/get_create_task_runner_function', () => ({
|
|||
import { AlertTypeRegistry } from './alert_type_registry';
|
||||
import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
||||
import { taskManagerMock } from '../../task_manager/task_manager.mock';
|
||||
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/plugin.mock';
|
||||
|
||||
const taskManager = taskManagerMock.create();
|
||||
|
||||
const alertTypeRegistryParams = {
|
||||
isSecurityEnabled: true,
|
||||
getServices() {
|
||||
return {
|
||||
log: jest.fn(),
|
||||
|
@ -24,9 +26,9 @@ const alertTypeRegistryParams = {
|
|||
},
|
||||
taskManager,
|
||||
executeAction: jest.fn(),
|
||||
internalSavedObjectsRepository: SavedObjectsClientMock.create(),
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(),
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
@ -50,15 +52,16 @@ describe('has()', () => {
|
|||
|
||||
describe('register()', () => {
|
||||
test('registers the executor with the task manager', () => {
|
||||
const alertType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { getCreateTaskRunnerFunction } = require('./lib/get_create_task_runner_function');
|
||||
const registry = new AlertTypeRegistry(alertTypeRegistryParams);
|
||||
getCreateTaskRunnerFunction.mockReturnValue(jest.fn());
|
||||
registry.register({
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
executor: jest.fn(),
|
||||
});
|
||||
registry.register(alertType);
|
||||
expect(taskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.registerTaskDefinitions.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -71,19 +74,15 @@ Array [
|
|||
},
|
||||
]
|
||||
`);
|
||||
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
|
||||
const firstCall = getCreateTaskRunnerFunction.mock.calls[0][0];
|
||||
expect(firstCall.alertType).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"executor": [MockFunction],
|
||||
"id": "test",
|
||||
"name": "Test",
|
||||
}
|
||||
`);
|
||||
expect(firstCall.internalSavedObjectsRepository).toBeTruthy();
|
||||
expect(firstCall.getBasePath).toBeTruthy();
|
||||
expect(firstCall.spaceIdToNamespace).toBeTruthy();
|
||||
expect(firstCall.executeAction).toMatchInlineSnapshot(`[MockFunction]`);
|
||||
expect(getCreateTaskRunnerFunction).toHaveBeenCalledWith({
|
||||
alertType,
|
||||
isSecurityEnabled: true,
|
||||
getServices: alertTypeRegistryParams.getServices,
|
||||
encryptedSavedObjectsPlugin: alertTypeRegistryParams.encryptedSavedObjectsPlugin,
|
||||
getBasePath: alertTypeRegistryParams.getBasePath,
|
||||
spaceIdToNamespace: alertTypeRegistryParams.spaceIdToNamespace,
|
||||
executeAction: alertTypeRegistryParams.executeAction,
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw an error if type is already registered', () => {
|
||||
|
|
|
@ -6,45 +6,53 @@
|
|||
|
||||
import Boom from 'boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { AlertType, Services } from './types';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { getCreateTaskRunnerFunction } from './lib';
|
||||
import { ActionsPlugin } from '../../actions';
|
||||
import { SpacesPlugin } from '../../spaces';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
|
||||
import {
|
||||
AlertType,
|
||||
GetBasePathFunction,
|
||||
GetServicesFunction,
|
||||
SpaceIdToNamespaceFunction,
|
||||
} from './types';
|
||||
|
||||
interface ConstructorOptions {
|
||||
getServices: (basePath: string) => Services;
|
||||
isSecurityEnabled: boolean;
|
||||
getServices: GetServicesFunction;
|
||||
taskManager: TaskManager;
|
||||
executeAction: ActionsPlugin['execute'];
|
||||
internalSavedObjectsRepository: SavedObjectsClientContract;
|
||||
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
|
||||
getBasePath: SpacesPlugin['getBasePath'];
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
getBasePath: GetBasePathFunction;
|
||||
}
|
||||
|
||||
export class AlertTypeRegistry {
|
||||
private readonly getServices: (basePath: string) => Services;
|
||||
private readonly getServices: GetServicesFunction;
|
||||
private readonly taskManager: TaskManager;
|
||||
private readonly executeAction: ActionsPlugin['execute'];
|
||||
private readonly alertTypes: Map<string, AlertType> = new Map();
|
||||
private readonly internalSavedObjectsRepository: SavedObjectsClientContract;
|
||||
private readonly spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
|
||||
private readonly getBasePath: SpacesPlugin['getBasePath'];
|
||||
private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
private readonly getBasePath: GetBasePathFunction;
|
||||
private readonly isSecurityEnabled: boolean;
|
||||
|
||||
constructor({
|
||||
internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsPlugin,
|
||||
executeAction,
|
||||
taskManager,
|
||||
getServices,
|
||||
spaceIdToNamespace,
|
||||
getBasePath,
|
||||
isSecurityEnabled,
|
||||
}: ConstructorOptions) {
|
||||
this.taskManager = taskManager;
|
||||
this.executeAction = executeAction;
|
||||
this.internalSavedObjectsRepository = internalSavedObjectsRepository;
|
||||
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
|
||||
this.getServices = getServices;
|
||||
this.getBasePath = getBasePath;
|
||||
this.spaceIdToNamespace = spaceIdToNamespace;
|
||||
this.isSecurityEnabled = isSecurityEnabled;
|
||||
}
|
||||
|
||||
public has(id: string) {
|
||||
|
@ -69,9 +77,10 @@ export class AlertTypeRegistry {
|
|||
type: `alerting:${alertType.id}`,
|
||||
createTaskRunner: getCreateTaskRunnerFunction({
|
||||
alertType,
|
||||
isSecurityEnabled: this.isSecurityEnabled,
|
||||
getServices: this.getServices,
|
||||
executeAction: this.executeAction,
|
||||
internalSavedObjectsRepository: this.internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
|
||||
getBasePath: this.getBasePath,
|
||||
spaceIdToNamespace: this.spaceIdToNamespace,
|
||||
}),
|
||||
|
|
|
@ -20,9 +20,15 @@ const alertsClientParams = {
|
|||
alertTypeRegistry,
|
||||
savedObjectsClient,
|
||||
spaceId: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertsClientParams.createAPIKey.mockResolvedValue({ created: false });
|
||||
alertsClientParams.getUserName.mockResolvedValue('elastic');
|
||||
});
|
||||
|
||||
const mockedDate = new Date('2019-02-12T21:01:22.479Z');
|
||||
(global as any).Date = class Date {
|
||||
|
@ -119,98 +125,101 @@ describe('create()', () => {
|
|||
});
|
||||
const result = await alertsClient.create({ data });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3);
|
||||
expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert');
|
||||
expect(savedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionRef": "action_0",
|
||||
"group": "default",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"enabled": true,
|
||||
"interval": "10s",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionRef": "action_0",
|
||||
"group": "default",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"apiKey": undefined,
|
||||
"createdBy": "elastic",
|
||||
"enabled": true,
|
||||
"interval": "10s",
|
||||
"updatedBy": "elastic",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(taskManager.schedule).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"params": Object {
|
||||
"alertId": "1",
|
||||
"spaceId": "default",
|
||||
},
|
||||
"scope": Array [
|
||||
"alerting",
|
||||
],
|
||||
"state": Object {
|
||||
"alertInstances": Object {},
|
||||
"alertTypeState": Object {},
|
||||
"previousStartedAt": null,
|
||||
},
|
||||
"taskType": "alerting:123",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"params": Object {
|
||||
"alertId": "1",
|
||||
"spaceId": "default",
|
||||
},
|
||||
"scope": Array [
|
||||
"alerting",
|
||||
],
|
||||
"state": Object {
|
||||
"alertInstances": Object {},
|
||||
"alertTypeState": Object {},
|
||||
"previousStartedAt": null,
|
||||
},
|
||||
"taskType": "alerting:123",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4);
|
||||
expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
|
||||
expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
|
||||
expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('creates a disabled alert', async () => {
|
||||
|
@ -251,25 +260,25 @@ describe('create()', () => {
|
|||
});
|
||||
const result = await alertsClient.create({ data });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"enabled": false,
|
||||
"id": "1",
|
||||
"interval": 10000,
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"enabled": false,
|
||||
"id": "1",
|
||||
"interval": 10000,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.schedule).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
@ -350,11 +359,11 @@ describe('create()', () => {
|
|||
);
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('returns task manager error if cleanup fails, logs to console', async () => {
|
||||
|
@ -399,14 +408,14 @@ describe('create()', () => {
|
|||
);
|
||||
expect(alertsClientParams.log).toHaveBeenCalledTimes(1);
|
||||
expect(alertsClientParams.log.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"alerting",
|
||||
"error",
|
||||
],
|
||||
"Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Array [
|
||||
"alerting",
|
||||
"error",
|
||||
],
|
||||
"Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws an error if alert type not registerd', async () => {
|
||||
|
@ -419,6 +428,104 @@ describe('create()', () => {
|
|||
`"Invalid type"`
|
||||
);
|
||||
});
|
||||
|
||||
test('calls the API key function', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
const data = getMockData();
|
||||
alertTypeRegistry.get.mockReturnValueOnce({
|
||||
id: '123',
|
||||
name: 'Test',
|
||||
async executor() {},
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
interval: '10s',
|
||||
alertTypeParams: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
taskManager.schedule.mockResolvedValueOnce({
|
||||
id: 'task-123',
|
||||
taskType: 'alerting:123',
|
||||
scheduledAt: new Date(),
|
||||
attempts: 1,
|
||||
status: 'idle',
|
||||
runAt: new Date(),
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
state: {},
|
||||
params: {},
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
scheduledTaskId: 'task-123',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
});
|
||||
await alertsClient.create({ data });
|
||||
|
||||
expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
actionRef: 'action_0',
|
||||
group: 'default',
|
||||
params: { foo: true },
|
||||
},
|
||||
],
|
||||
alertTypeId: '123',
|
||||
alertTypeParams: { bar: true },
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
enabled: true,
|
||||
interval: '10s',
|
||||
},
|
||||
{
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enable()', () => {
|
||||
|
@ -454,6 +561,8 @@ describe('enable()', () => {
|
|||
{
|
||||
enabled: true,
|
||||
scheduledTaskId: 'task-123',
|
||||
updatedBy: 'elastic',
|
||||
apiKey: null,
|
||||
},
|
||||
{
|
||||
references: [],
|
||||
|
@ -491,6 +600,64 @@ describe('enable()', () => {
|
|||
expect(taskManager.schedule).toHaveBeenCalledTimes(0);
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('calls the API key function', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
interval: '10s',
|
||||
alertTypeId: '2',
|
||||
enabled: false,
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
taskManager.schedule.mockResolvedValueOnce({
|
||||
id: 'task-123',
|
||||
scheduledAt: new Date(),
|
||||
attempts: 0,
|
||||
status: 'idle',
|
||||
runAt: new Date(),
|
||||
state: {},
|
||||
params: {},
|
||||
taskType: '',
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
|
||||
await alertsClient.enable({ id: '1' });
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
'1',
|
||||
{
|
||||
enabled: true,
|
||||
scheduledTaskId: 'task-123',
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
updatedBy: 'elastic',
|
||||
},
|
||||
{
|
||||
references: [],
|
||||
}
|
||||
);
|
||||
expect(taskManager.schedule).toHaveBeenCalledWith({
|
||||
taskType: `alerting:2`,
|
||||
params: {
|
||||
alertId: '1',
|
||||
spaceId: 'default',
|
||||
},
|
||||
state: {
|
||||
alertInstances: {},
|
||||
alertTypeState: {},
|
||||
previousStartedAt: null,
|
||||
},
|
||||
scope: ['alerting'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable()', () => {
|
||||
|
@ -513,8 +680,10 @@ describe('disable()', () => {
|
|||
'alert',
|
||||
'1',
|
||||
{
|
||||
apiKey: null,
|
||||
enabled: false,
|
||||
scheduledTaskId: null,
|
||||
updatedBy: 'elastic',
|
||||
},
|
||||
{
|
||||
references: [],
|
||||
|
@ -575,31 +744,31 @@ describe('get()', () => {
|
|||
});
|
||||
const result = await alertsClient.get({ id: '1' });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test(`throws an error when references aren't found`, async () => {
|
||||
|
@ -670,39 +839,39 @@ describe('find()', () => {
|
|||
});
|
||||
const result = await alertsClient.find();
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"data": Array [
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
},
|
||||
],
|
||||
"page": 1,
|
||||
"perPage": 10,
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"data": Array [
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
},
|
||||
],
|
||||
"page": 1,
|
||||
"perPage": 10,
|
||||
"total": 1,
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"type": "alert",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"type": "alert",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -744,17 +913,17 @@ describe('delete()', () => {
|
|||
expect(result).toEqual({ success: true });
|
||||
expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
expect(taskManager.remove).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.remove.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"task-123",
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"task-123",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -826,58 +995,189 @@ describe('update()', () => {
|
|||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4);
|
||||
expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
|
||||
expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
|
||||
expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionRef": "action_0",
|
||||
"group": "default",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"interval": "10s",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionRef": "action_0",
|
||||
"group": "default",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"apiKey": null,
|
||||
"interval": "10s",
|
||||
"updatedBy": "elastic",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
"version": "123",
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
"version": "123",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('calls the createApiKey function', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertTypeRegistry.get.mockReturnValueOnce({
|
||||
id: '123',
|
||||
name: 'Test',
|
||||
async executor() {},
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
alertTypeId: '123',
|
||||
scheduledTaskId: 'task-123',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
interval: '10s',
|
||||
alertTypeParams: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
scheduledTaskId: 'task-123',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
interval: '10s',
|
||||
alertTypeParams: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
version: '123',
|
||||
},
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"enabled": true,
|
||||
"id": "1",
|
||||
"interval": "10s",
|
||||
"scheduledTaskId": "task-123",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4);
|
||||
expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
|
||||
expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1');
|
||||
expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"actionRef": "action_0",
|
||||
"group": "default",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeParams": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"interval": "10s",
|
||||
"updatedBy": "elastic",
|
||||
}
|
||||
`);
|
||||
expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"name": "action_0",
|
||||
"type": "action",
|
||||
},
|
||||
],
|
||||
"version": "123",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should validate alertTypeParams', async () => {
|
||||
|
|
|
@ -9,6 +9,16 @@ import { SavedObjectsClientContract, SavedObjectReference } from 'src/core/serve
|
|||
import { Alert, RawAlert, AlertTypeRegistry, AlertAction, Log } from './types';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { validateAlertTypeParams } from './lib';
|
||||
import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult } from '../../../../plugins/security/server';
|
||||
|
||||
interface FailedCreateAPIKeyResult {
|
||||
created: false;
|
||||
}
|
||||
interface SuccessCreateAPIKeyResult {
|
||||
created: true;
|
||||
result: SecurityPluginCreateAPIKeyResult;
|
||||
}
|
||||
export type CreateAPIKeyResult = FailedCreateAPIKeyResult | SuccessCreateAPIKeyResult;
|
||||
|
||||
interface ConstructorOptions {
|
||||
log: Log;
|
||||
|
@ -16,6 +26,8 @@ interface ConstructorOptions {
|
|||
savedObjectsClient: SavedObjectsClientContract;
|
||||
alertTypeRegistry: AlertTypeRegistry;
|
||||
spaceId?: string;
|
||||
getUserName: () => Promise<string | null>;
|
||||
createAPIKey: () => Promise<CreateAPIKeyResult>;
|
||||
}
|
||||
|
||||
interface FindOptions {
|
||||
|
@ -42,7 +54,7 @@ interface FindResult {
|
|||
}
|
||||
|
||||
interface CreateOptions {
|
||||
data: Alert;
|
||||
data: Pick<Alert, Exclude<keyof Alert, 'createdBy' | 'updatedBy' | 'apiKey'>>;
|
||||
options?: {
|
||||
migrationVersion?: Record<string, string>;
|
||||
};
|
||||
|
@ -60,10 +72,12 @@ interface UpdateOptions {
|
|||
|
||||
export class AlertsClient {
|
||||
private readonly log: Log;
|
||||
private readonly getUserName: () => Promise<string | null>;
|
||||
private readonly spaceId?: string;
|
||||
private readonly taskManager: TaskManager;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly alertTypeRegistry: AlertTypeRegistry;
|
||||
private readonly createAPIKey: () => Promise<CreateAPIKeyResult>;
|
||||
|
||||
constructor({
|
||||
alertTypeRegistry,
|
||||
|
@ -71,20 +85,31 @@ export class AlertsClient {
|
|||
taskManager,
|
||||
log,
|
||||
spaceId,
|
||||
getUserName,
|
||||
createAPIKey,
|
||||
}: ConstructorOptions) {
|
||||
this.log = log;
|
||||
this.getUserName = getUserName;
|
||||
this.spaceId = spaceId;
|
||||
this.taskManager = taskManager;
|
||||
this.alertTypeRegistry = alertTypeRegistry;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.createAPIKey = createAPIKey;
|
||||
}
|
||||
|
||||
public async create({ data, options }: CreateOptions) {
|
||||
// Throws an error if alert type isn't registered
|
||||
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
|
||||
const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams);
|
||||
const apiKey = await this.createAPIKey();
|
||||
const username = await this.getUserName();
|
||||
const { alert: rawAlert, references } = this.getRawAlert({
|
||||
...data,
|
||||
createdBy: username,
|
||||
updatedBy: username,
|
||||
apiKey: apiKey.created
|
||||
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
|
||||
: undefined,
|
||||
alertTypeParams: validatedAlertTypeParams,
|
||||
});
|
||||
const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, {
|
||||
|
@ -161,6 +186,7 @@ export class AlertsClient {
|
|||
const existingObject = await this.savedObjectsClient.get('alert', id);
|
||||
const { alertTypeId } = existingObject.attributes;
|
||||
const alertType = this.alertTypeRegistry.get(alertTypeId);
|
||||
const apiKey = await this.createAPIKey();
|
||||
|
||||
// Validate
|
||||
const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.alertTypeParams);
|
||||
|
@ -173,6 +199,10 @@ export class AlertsClient {
|
|||
...data,
|
||||
alertTypeParams: validatedAlertTypeParams,
|
||||
actions,
|
||||
apiKey: apiKey.created
|
||||
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
|
||||
: null,
|
||||
updatedBy: await this.getUserName(),
|
||||
},
|
||||
{
|
||||
...options,
|
||||
|
@ -185,6 +215,7 @@ export class AlertsClient {
|
|||
public async enable({ id }: { id: string }) {
|
||||
const existingObject = await this.savedObjectsClient.get('alert', id);
|
||||
if (existingObject.attributes.enabled === false) {
|
||||
const apiKey = await this.createAPIKey();
|
||||
const scheduledTask = await this.scheduleAlert(
|
||||
id,
|
||||
existingObject.attributes.alertTypeId,
|
||||
|
@ -195,7 +226,11 @@ export class AlertsClient {
|
|||
id,
|
||||
{
|
||||
enabled: true,
|
||||
updatedBy: await this.getUserName(),
|
||||
scheduledTaskId: scheduledTask.id,
|
||||
apiKey: apiKey.created
|
||||
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
|
||||
: null,
|
||||
},
|
||||
{ references: existingObject.references }
|
||||
);
|
||||
|
@ -205,16 +240,18 @@ export class AlertsClient {
|
|||
public async disable({ id }: { id: string }) {
|
||||
const existingObject = await this.savedObjectsClient.get('alert', id);
|
||||
if (existingObject.attributes.enabled === true) {
|
||||
await this.taskManager.remove(existingObject.attributes.scheduledTaskId);
|
||||
await this.savedObjectsClient.update(
|
||||
'alert',
|
||||
id,
|
||||
{
|
||||
enabled: false,
|
||||
scheduledTaskId: null,
|
||||
apiKey: null,
|
||||
updatedBy: await this.getUserName(),
|
||||
},
|
||||
{ references: existingObject.references }
|
||||
);
|
||||
await this.taskManager.remove(existingObject.attributes.scheduledTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,20 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Hapi from 'hapi';
|
||||
import uuid from 'uuid';
|
||||
import { Legacy } from 'kibana';
|
||||
import KbnServer from 'src/legacy/server/kbn_server';
|
||||
import { ActionsPlugin } from '../../actions';
|
||||
import { TaskManager } from '../../task_manager';
|
||||
import { AlertingPlugin, Services } from './types';
|
||||
import { AlertTypeRegistry } from './alert_type_registry';
|
||||
import { AlertsClient, CreateAPIKeyResult } from './alerts_client';
|
||||
import { SpacesPlugin } from '../../spaces';
|
||||
import { KibanaRequest } from '../../../../../src/core/server';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
|
||||
import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../plugins/security/server';
|
||||
import { createOptionalPlugin } from '../../../server/lib/optional_plugin';
|
||||
import {
|
||||
createAlertRoute,
|
||||
deleteAlertRoute,
|
||||
|
@ -15,52 +28,92 @@ import {
|
|||
enableAlertRoute,
|
||||
disableAlertRoute,
|
||||
} from './routes';
|
||||
import { AlertingPlugin, Services } from './types';
|
||||
import { AlertTypeRegistry } from './alert_type_registry';
|
||||
import { AlertsClient } from './alerts_client';
|
||||
import { SpacesPlugin } from '../../spaces';
|
||||
import { createOptionalPlugin } from '../../../server/lib/optional_plugin';
|
||||
|
||||
export function init(server: Legacy.Server) {
|
||||
// Extend PluginProperties to indicate which plugins are guaranteed to exist
|
||||
// due to being marked as dependencies
|
||||
interface Plugins extends Hapi.PluginProperties {
|
||||
actions: ActionsPlugin;
|
||||
task_manager: TaskManager;
|
||||
encrypted_saved_objects: EncryptedSavedObjectsPlugin;
|
||||
}
|
||||
|
||||
interface Server extends Legacy.Server {
|
||||
plugins: Plugins;
|
||||
}
|
||||
|
||||
export function init(server: Server) {
|
||||
const config = server.config();
|
||||
const kbnServer = (server as unknown) as KbnServer;
|
||||
const taskManager = server.plugins.task_manager;
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const spaces = createOptionalPlugin<SpacesPlugin>(
|
||||
config,
|
||||
'xpack.spaces',
|
||||
server.plugins,
|
||||
'spaces'
|
||||
);
|
||||
|
||||
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
|
||||
const savedObjectsRepositoryWithInternalUser = server.savedObjects.getSavedObjectsRepository(
|
||||
callWithInternalUser
|
||||
const security = createOptionalPlugin<SecurityPluginSetupContract>(
|
||||
config,
|
||||
'xpack.security',
|
||||
kbnServer.newPlatform.setup.plugins,
|
||||
'security'
|
||||
);
|
||||
|
||||
function getServices(basePath: string): Services {
|
||||
const fakeRequest: any = {
|
||||
headers: {},
|
||||
getBasePath: () => basePath,
|
||||
};
|
||||
server.plugins.xpack_main.registerFeature({
|
||||
id: 'alerting',
|
||||
name: 'Alerting',
|
||||
app: ['alerting', 'kibana'],
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['alert'],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
api: ['alerting-read', 'alerting-all'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['alert'],
|
||||
},
|
||||
ui: [],
|
||||
api: ['alerting-read'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Encrypted attributes
|
||||
server.plugins.encrypted_saved_objects.registerType({
|
||||
type: 'alert',
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
attributesToExcludeFromAAD: new Set(['scheduledTaskId']),
|
||||
});
|
||||
|
||||
function getServices(request: any): Services {
|
||||
return {
|
||||
log: server.log.bind(server),
|
||||
callCluster: callWithInternalUser,
|
||||
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(fakeRequest),
|
||||
log: (...args) => server.log(...args),
|
||||
callCluster: (...args) => callWithRequest(request, ...args),
|
||||
savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(request),
|
||||
};
|
||||
}
|
||||
function getBasePath(spaceId?: string): string {
|
||||
return spaces.isEnabled && spaceId
|
||||
? spaces.getBasePath(spaceId)
|
||||
: ((server.config().get('server.basePath') || '') as string);
|
||||
}
|
||||
function spaceIdToNamespace(spaceId?: string): string | undefined {
|
||||
return spaces.isEnabled && spaceId ? spaces.spaceIdToNamespace(spaceId) : undefined;
|
||||
}
|
||||
|
||||
const taskManager = server.plugins.task_manager!;
|
||||
const alertTypeRegistry = new AlertTypeRegistry({
|
||||
getServices,
|
||||
taskManager: taskManager!,
|
||||
executeAction: server.plugins.actions!.execute,
|
||||
internalSavedObjectsRepository: savedObjectsRepositoryWithInternalUser,
|
||||
getBasePath(...args) {
|
||||
return spaces.isEnabled
|
||||
? spaces.getBasePath(...args)
|
||||
: server.config().get('server.basePath');
|
||||
},
|
||||
spaceIdToNamespace(...args) {
|
||||
return spaces.isEnabled ? spaces.spaceIdToNamespace(...args) : undefined;
|
||||
},
|
||||
isSecurityEnabled: security.isEnabled,
|
||||
taskManager,
|
||||
executeAction: server.plugins.actions.execute,
|
||||
encryptedSavedObjectsPlugin: server.plugins.encrypted_saved_objects,
|
||||
getBasePath,
|
||||
spaceIdToNamespace,
|
||||
});
|
||||
|
||||
// Register routes
|
||||
|
@ -77,15 +130,37 @@ export function init(server: Legacy.Server) {
|
|||
server.decorate('request', 'getAlertsClient', function() {
|
||||
const request = this;
|
||||
const savedObjectsClient = request.getSavedObjectsClient();
|
||||
|
||||
const alertsClient = new AlertsClient({
|
||||
log: server.log.bind(server),
|
||||
savedObjectsClient,
|
||||
alertTypeRegistry,
|
||||
taskManager: taskManager!,
|
||||
taskManager,
|
||||
spaceId: spaces.isEnabled ? spaces.getSpaceId(request) : undefined,
|
||||
async getUserName(): Promise<string | null> {
|
||||
if (!security.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
const user = await security.authc.getCurrentUser(KibanaRequest.from(request));
|
||||
return user ? user.username : null;
|
||||
},
|
||||
async createAPIKey(): Promise<CreateAPIKeyResult> {
|
||||
if (!security.isEnabled) {
|
||||
return { created: false };
|
||||
}
|
||||
return {
|
||||
created: true,
|
||||
result: (await security.authc.createAPIKey(KibanaRequest.from(request), {
|
||||
name: `source: alerting, generated uuid: "${uuid.v4()}"`,
|
||||
role_descriptors: {},
|
||||
}))!,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return alertsClient;
|
||||
});
|
||||
|
||||
const exposedFunctions: AlertingPlugin = {
|
||||
registerType: alertTypeRegistry.register.bind(alertTypeRegistry),
|
||||
listTypes: alertTypeRegistry.list.bind(alertTypeRegistry),
|
||||
|
|
|
@ -9,37 +9,20 @@ import { createFireHandler } from './create_fire_handler';
|
|||
const createFireHandlerParams = {
|
||||
executeAction: jest.fn(),
|
||||
spaceId: 'default',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
alertSavedObject: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
interval: '10s',
|
||||
alertTypeParams: {
|
||||
bar: true,
|
||||
actions: [
|
||||
{
|
||||
id: '1',
|
||||
group: 'default',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
@ -51,6 +34,7 @@ test('calls executeAction per selected action', async () => {
|
|||
expect(createFireHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"contextVal": "My goes here",
|
||||
|
@ -76,6 +60,7 @@ test('context attribute gets parameterized', async () => {
|
|||
expect(createFireHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"contextVal": "My context-val goes here",
|
||||
|
@ -95,6 +80,7 @@ test('state attribute gets parameterized', async () => {
|
|||
expect(createFireHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"contextVal": "My goes here",
|
||||
|
@ -106,39 +92,3 @@ test('state attribute gets parameterized', async () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws error if reference not found', async () => {
|
||||
const params = {
|
||||
spaceId: 'default',
|
||||
executeAction: jest.fn(),
|
||||
alertSavedObject: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
interval: '10s',
|
||||
alertTypeParams: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
contextVal: 'My {{context.value}} goes here',
|
||||
stateVal: 'My {{state.value}} goes here',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
};
|
||||
const fireHandler = createFireHandler(params);
|
||||
await expect(
|
||||
fireHandler('default', {}, { value: 'state-val' })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Action reference \\"action_0\\" not found in alert id: 1"`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -4,38 +4,29 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObject } from 'src/core/server';
|
||||
import { RawAlertAction, State, Context } from '../types';
|
||||
import { AlertAction, State, Context } from '../types';
|
||||
import { ActionsPlugin } from '../../../actions';
|
||||
import { transformActionParams } from './transform_action_params';
|
||||
|
||||
interface CreateFireHandlerOptions {
|
||||
executeAction: ActionsPlugin['execute'];
|
||||
alertSavedObject: SavedObject;
|
||||
actions: AlertAction[];
|
||||
spaceId: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export function createFireHandler({
|
||||
executeAction,
|
||||
alertSavedObject,
|
||||
actions: alertActions,
|
||||
spaceId,
|
||||
apiKey,
|
||||
}: CreateFireHandlerOptions) {
|
||||
return async (actionGroup: string, context: Context, state: State) => {
|
||||
const alertActions: RawAlertAction[] = alertSavedObject.attributes.actions;
|
||||
const actions = alertActions
|
||||
.filter(({ group }) => group === actionGroup)
|
||||
.map(action => {
|
||||
const actionReference = alertSavedObject.references.find(
|
||||
obj => obj.name === action.actionRef
|
||||
);
|
||||
if (!actionReference) {
|
||||
throw new Error(
|
||||
`Action reference "${action.actionRef}" not found in alert id: ${alertSavedObject.id}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
id: actionReference.id,
|
||||
params: transformActionParams(action.params, state, context),
|
||||
};
|
||||
});
|
||||
|
@ -44,6 +35,7 @@ export function createFireHandler({
|
|||
id: action.id,
|
||||
params: action.params,
|
||||
spaceId,
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AlertExecutorOptions } from '../types';
|
|||
import { ConcreteTaskInstance } from '../../../task_manager';
|
||||
import { SavedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
import { getCreateTaskRunnerFunction } from './get_create_task_runner_function';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
|
||||
|
||||
let fakeTimer: sinon.SinonFakeTimers;
|
||||
let mockedTaskInstance: ConcreteTaskInstance;
|
||||
|
@ -38,22 +39,23 @@ beforeAll(() => {
|
|||
afterAll(() => fakeTimer.restore());
|
||||
|
||||
const savedObjectsClient = SavedObjectsClientMock.create();
|
||||
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
|
||||
const services = {
|
||||
log: jest.fn(),
|
||||
callCluster: jest.fn(),
|
||||
savedObjectsClient,
|
||||
};
|
||||
|
||||
const getCreateTaskRunnerFunctionParams = {
|
||||
getServices() {
|
||||
return {
|
||||
log: jest.fn(),
|
||||
callCluster: jest.fn(),
|
||||
savedObjectsClient: SavedObjectsClientMock.create(),
|
||||
};
|
||||
},
|
||||
isSecurityEnabled: true,
|
||||
getServices: jest.fn().mockReturnValue(services),
|
||||
alertType: {
|
||||
id: 'test',
|
||||
name: 'My test alert',
|
||||
executor: jest.fn(),
|
||||
},
|
||||
executeAction: jest.fn(),
|
||||
internalSavedObjectsRepository: savedObjectsClient,
|
||||
encryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
|
||||
getBasePath: jest.fn().mockReturnValue(undefined),
|
||||
};
|
||||
|
@ -87,30 +89,41 @@ const mockedAlertTypeSavedObject = {
|
|||
],
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
getCreateTaskRunnerFunctionParams.getServices.mockReturnValue(services);
|
||||
});
|
||||
|
||||
test('successfully executes the task', async () => {
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
|
||||
const runnerResult = await runner.run();
|
||||
expect(runnerResult).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"runAt": 1970-01-01T00:00:10.000Z,
|
||||
"state": Object {
|
||||
"alertInstances": Object {},
|
||||
"alertTypeState": undefined,
|
||||
"previousStartedAt": 1970-01-01T00:00:00.000Z,
|
||||
},
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"runAt": 1970-01-01T00:00:10.000Z,
|
||||
"state": Object {
|
||||
"alertInstances": Object {},
|
||||
"alertTypeState": undefined,
|
||||
"previousStartedAt": 1970-01-01T00:00:00.000Z,
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(getCreateTaskRunnerFunctionParams.alertType.executor).toHaveBeenCalledTimes(1);
|
||||
const call = getCreateTaskRunnerFunctionParams.alertType.executor.mock.calls[0][0];
|
||||
expect(call.params).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bar": true,
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"bar": true,
|
||||
}
|
||||
`);
|
||||
expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`);
|
||||
expect(call.state).toMatchInlineSnapshot(`Object {}`);
|
||||
expect(call.services.alertInstanceFactory).toBeTruthy();
|
||||
|
@ -120,18 +133,27 @@ test('successfully executes the task', async () => {
|
|||
|
||||
test('fireAction is called per alert instance that fired', async () => {
|
||||
getCreateTaskRunnerFunctionParams.alertType.executor.mockImplementation(
|
||||
({ services }: AlertExecutorOptions) => {
|
||||
services.alertInstanceFactory('1').fire('default');
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices.alertInstanceFactory('1').fire('default');
|
||||
}
|
||||
);
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
|
||||
await runner.run();
|
||||
expect(getCreateTaskRunnerFunctionParams.executeAction).toHaveBeenCalledTimes(1);
|
||||
expect(getCreateTaskRunnerFunctionParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"apiKey": "MTIzOmFiYw==",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
|
@ -144,12 +166,20 @@ test('fireAction is called per alert instance that fired', async () => {
|
|||
|
||||
test('persists alertInstances passed in from state, only if they fire', async () => {
|
||||
getCreateTaskRunnerFunctionParams.alertType.executor.mockImplementation(
|
||||
({ services }: AlertExecutorOptions) => {
|
||||
services.alertInstanceFactory('1').fire('default');
|
||||
({ services: executorServices }: AlertExecutorOptions) => {
|
||||
executorServices.alertInstanceFactory('1').fire('default');
|
||||
}
|
||||
);
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({
|
||||
taskInstance: {
|
||||
...mockedTaskInstance,
|
||||
|
@ -164,17 +194,17 @@ test('persists alertInstances passed in from state, only if they fire', async ()
|
|||
});
|
||||
const runnerResult = await runner.run();
|
||||
expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"lastFired": 0,
|
||||
},
|
||||
"state": Object {
|
||||
"bar": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
Object {
|
||||
"1": Object {
|
||||
"meta": Object {
|
||||
"lastFired": 0,
|
||||
},
|
||||
"state": Object {
|
||||
"bar": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('validates params before executing the alert type', async () => {
|
||||
|
@ -190,8 +220,81 @@ test('validates params before executing the alert type', async () => {
|
|||
},
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
|
||||
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws error if reference not found', async () => {
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
...mockedAlertTypeSavedObject,
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
|
||||
await expect(runner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Action reference \\"action_0\\" not found in alert id: 1"`
|
||||
);
|
||||
});
|
||||
|
||||
test('uses API key when provided', async () => {
|
||||
const createTaskRunner = getCreateTaskRunnerFunction(getCreateTaskRunnerFunctionParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
|
||||
|
||||
await runner.run();
|
||||
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`doesn't use API key when not provided`, async () => {
|
||||
const createTaskRunner = getCreateTaskRunnerFunction({
|
||||
...getCreateTaskRunnerFunctionParams,
|
||||
isSecurityEnabled: false,
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
const runner = createTaskRunner({ taskInstance: mockedTaskInstance });
|
||||
|
||||
await runner.run();
|
||||
|
||||
expect(getCreateTaskRunnerFunctionParams.getServices).toHaveBeenCalledWith({
|
||||
getBasePath: expect.anything(),
|
||||
headers: {},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,24 +4,31 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { ActionsPlugin } from '../../../actions';
|
||||
import { AlertType, Services, AlertServices } from '../types';
|
||||
import { ConcreteTaskInstance } from '../../../task_manager';
|
||||
import { createFireHandler } from './create_fire_handler';
|
||||
import { createAlertInstanceFactory } from './create_alert_instance_factory';
|
||||
import { AlertInstance } from './alert_instance';
|
||||
import { getNextRunAt } from './get_next_run_at';
|
||||
import { validateAlertTypeParams } from './validate_alert_type_params';
|
||||
import { SpacesPlugin } from '../../../spaces';
|
||||
import { EncryptedSavedObjectsPlugin } from '../../../encrypted_saved_objects';
|
||||
import {
|
||||
AlertType,
|
||||
AlertServices,
|
||||
GetBasePathFunction,
|
||||
GetServicesFunction,
|
||||
RawAlert,
|
||||
SpaceIdToNamespaceFunction,
|
||||
} from '../types';
|
||||
|
||||
interface CreateTaskRunnerFunctionOptions {
|
||||
getServices: (basePath: string) => Services;
|
||||
isSecurityEnabled: boolean;
|
||||
getServices: GetServicesFunction;
|
||||
alertType: AlertType;
|
||||
executeAction: ActionsPlugin['execute'];
|
||||
internalSavedObjectsRepository: SavedObjectsClientContract;
|
||||
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
|
||||
getBasePath: SpacesPlugin['getBasePath'];
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
getBasePath: GetBasePathFunction;
|
||||
}
|
||||
|
||||
interface TaskRunnerOptions {
|
||||
|
@ -32,30 +39,67 @@ export function getCreateTaskRunnerFunction({
|
|||
getServices,
|
||||
alertType,
|
||||
executeAction,
|
||||
internalSavedObjectsRepository,
|
||||
encryptedSavedObjectsPlugin,
|
||||
spaceIdToNamespace,
|
||||
getBasePath,
|
||||
isSecurityEnabled,
|
||||
}: CreateTaskRunnerFunctionOptions) {
|
||||
return ({ taskInstance }: TaskRunnerOptions) => {
|
||||
return {
|
||||
run: async () => {
|
||||
const namespace = spaceIdToNamespace(taskInstance.params.spaceId);
|
||||
const alertSavedObject = await internalSavedObjectsRepository.get(
|
||||
const { alertId, spaceId } = taskInstance.params;
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
const namespace = spaceIdToNamespace(spaceId);
|
||||
// Only fetch encrypted attributes here, we'll create a saved objects client
|
||||
// scoped with the API key to fetch the remaining data.
|
||||
const {
|
||||
attributes: { apiKey },
|
||||
} = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAlert>(
|
||||
'alert',
|
||||
taskInstance.params.alertId,
|
||||
alertId,
|
||||
{ namespace }
|
||||
);
|
||||
|
||||
if (isSecurityEnabled && !apiKey) {
|
||||
throw new Error('API key is required. The attribute "apiKey" is missing.');
|
||||
} else if (isSecurityEnabled) {
|
||||
requestHeaders.authorization = `ApiKey ${apiKey}`;
|
||||
}
|
||||
|
||||
const fakeRequest = {
|
||||
headers: requestHeaders,
|
||||
getBasePath: () => getBasePath(spaceId),
|
||||
};
|
||||
|
||||
const services = getServices(fakeRequest);
|
||||
// Ensure API key is still valid and user has access
|
||||
const {
|
||||
attributes: { alertTypeParams, actions, interval },
|
||||
references,
|
||||
} = await services.savedObjectsClient.get<RawAlert>('alert', alertId);
|
||||
|
||||
// Validate
|
||||
const validatedAlertTypeParams = validateAlertTypeParams(
|
||||
alertType,
|
||||
alertSavedObject.attributes.alertTypeParams
|
||||
);
|
||||
const validatedAlertTypeParams = validateAlertTypeParams(alertType, alertTypeParams);
|
||||
|
||||
// Inject ids into actions
|
||||
const actionsWithIds = actions.map(action => {
|
||||
const actionReference = references.find(obj => obj.name === action.actionRef);
|
||||
if (!actionReference) {
|
||||
throw new Error(
|
||||
`Action reference "${action.actionRef}" not found in alert id: ${alertId}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
...action,
|
||||
id: actionReference.id,
|
||||
};
|
||||
});
|
||||
|
||||
const fireHandler = createFireHandler({
|
||||
alertSavedObject,
|
||||
executeAction,
|
||||
spaceId: taskInstance.params.spaceId,
|
||||
apiKey,
|
||||
actions: actionsWithIds,
|
||||
spaceId,
|
||||
});
|
||||
const alertInstances: Record<string, AlertInstance> = {};
|
||||
const alertInstancesData = taskInstance.state.alertInstances || {};
|
||||
|
@ -65,7 +109,7 @@ export function getCreateTaskRunnerFunction({
|
|||
const alertInstanceFactory = createAlertInstanceFactory(alertInstances);
|
||||
|
||||
const alertTypeServices: AlertServices = {
|
||||
...getServices(taskInstance.params.basePath),
|
||||
...services,
|
||||
alertInstanceFactory,
|
||||
};
|
||||
|
||||
|
@ -94,10 +138,7 @@ export function getCreateTaskRunnerFunction({
|
|||
})
|
||||
);
|
||||
|
||||
const nextRunAt = getNextRunAt(
|
||||
new Date(taskInstance.startedAt!),
|
||||
alertSavedObject.attributes.interval
|
||||
);
|
||||
const nextRunAt = getNextRunAt(new Date(taskInstance.startedAt!), interval);
|
||||
|
||||
return {
|
||||
state: {
|
||||
|
|
|
@ -24,6 +24,7 @@ export function createAlertRoute(server: Hapi.Server) {
|
|||
method: 'POST',
|
||||
path: '/api/alert',
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
|
|
|
@ -18,6 +18,7 @@ export function deleteAlertRoute(server: Hapi.Server) {
|
|||
method: 'DELETE',
|
||||
path: '/api/alert/{id}',
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
|
|
|
@ -11,6 +11,7 @@ export function disableAlertRoute(server: Hapi.Server) {
|
|||
method: 'POST',
|
||||
path: '/api/alert/{id}/_disable',
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
response: {
|
||||
emptyStatusCode: 204,
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ export function enableAlertRoute(server: Hapi.Server) {
|
|||
method: 'POST',
|
||||
path: '/api/alert/{id}/_enable',
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
response: {
|
||||
emptyStatusCode: 204,
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ export function findRoute(server: Hapi.Server) {
|
|||
method: 'GET',
|
||||
path: '/api/alert/_find',
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
validate: {
|
||||
query: Joi.object()
|
||||
.keys({
|
||||
|
|
|
@ -18,6 +18,7 @@ export function getRoute(server: Hapi.Server) {
|
|||
method: 'GET',
|
||||
path: `/api/alert/{id}`,
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
validate: {
|
||||
params: Joi.object()
|
||||
.keys({
|
||||
|
|
|
@ -10,6 +10,9 @@ export function listAlertTypesRoute(server: Hapi.Server) {
|
|||
server.route({
|
||||
method: 'GET',
|
||||
path: `/api/alert/types`,
|
||||
options: {
|
||||
tags: ['access:alerting-read'],
|
||||
},
|
||||
async handler(request: Hapi.Request) {
|
||||
return request.server.plugins.alerting!.listTypes();
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ export function updateAlertRoute(server: Hapi.Server) {
|
|||
method: 'PUT',
|
||||
path: '/api/alert/{id}',
|
||||
options: {
|
||||
tags: ['access:alerting-all'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
|
|
|
@ -11,6 +11,9 @@ import { AlertTypeRegistry } from './alert_type_registry';
|
|||
export type State = Record<string, any>;
|
||||
export type Context = Record<string, any>;
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (request: any) => Services;
|
||||
export type GetBasePathFunction = (spaceId?: string) => string;
|
||||
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
|
||||
|
||||
export type Log = (
|
||||
tags: string | string[],
|
||||
|
@ -66,6 +69,9 @@ export interface Alert {
|
|||
actions: AlertAction[];
|
||||
alertTypeParams: Record<string, any>;
|
||||
scheduledTaskId?: string;
|
||||
createdBy: string | null;
|
||||
updatedBy: string | null;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface RawAlert extends SavedObjectAttributes {
|
||||
|
@ -75,6 +81,9 @@ export interface RawAlert extends SavedObjectAttributes {
|
|||
actions: RawAlertAction[];
|
||||
alertTypeParams: SavedObjectAttributes;
|
||||
scheduledTaskId?: string;
|
||||
createdBy: string | null;
|
||||
updatedBy: string | null;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface AlertingPlugin {
|
||||
|
|
|
@ -21,6 +21,7 @@ export { Authenticator, ProviderLoginAttempt } from './authenticator';
|
|||
export { AuthenticationResult } from './authentication_result';
|
||||
export { DeauthenticationResult } from './deauthentication_result';
|
||||
export { OIDCAuthenticationFlow } from './providers';
|
||||
export { CreateAPIKeyResult } from './api_keys';
|
||||
|
||||
interface SetupAuthenticationParams {
|
||||
core: CoreSetup;
|
||||
|
|
|
@ -18,6 +18,7 @@ export {
|
|||
AuthenticationResult,
|
||||
DeauthenticationResult,
|
||||
OIDCAuthenticationFlow,
|
||||
CreateAPIKeyResult,
|
||||
} from './authentication';
|
||||
|
||||
export { PluginSetupContract } from './plugin';
|
||||
|
|
|
@ -12,7 +12,7 @@ require('@kbn/test').runTestsCli([
|
|||
require.resolve('../test/functional/config.js'),
|
||||
require.resolve('../test/api_integration/config_security_basic.js'),
|
||||
require.resolve('../test/api_integration/config.js'),
|
||||
require.resolve('../test/alerting_api_integration/config_security_enabled.js'),
|
||||
require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'),
|
||||
require.resolve('../test/plugin_api_integration/config.js'),
|
||||
require.resolve('../test/kerberos_api_integration/config'),
|
||||
require.resolve('../test/kerberos_api_integration/anonymous_access.config'),
|
||||
|
|
|
@ -1,134 +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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('create', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('should return 200 when creating an action and not return encrypted attributes', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: resp.body.id,
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
expect(typeof resp.body.id).to.be('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when creating an action inside a space and to not be accessible from another space', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/s/space_1/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(createdAction).to.eql({
|
||||
id: createdAction.id,
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
expect(typeof createdAction.id).to.be('string');
|
||||
await supertest.get(`/s/space_1/api/action/${createdAction.id}`).expect(200);
|
||||
await supertest.get(`/api/action/${createdAction.id}`).expect(404);
|
||||
});
|
||||
|
||||
it(`should return 400 when action type isn't registered`, async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.unregistered-action-type',
|
||||
config: {},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Action type "test.unregistered-action-type" is not registered.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when payload is empty and invalid', async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "description" fails because ["description" is required]. child "actionTypeId" fails because ["actionTypeId" is required]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['description', 'actionTypeId'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when config isn't valid`, async () => {
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'my description',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: 'my unencrypted text',
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [encrypted]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,55 +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 expect from '@kbn/expect';
|
||||
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function deleteActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('delete', () => {
|
||||
beforeEach(() => esArchiver.load('actions/basic'));
|
||||
afterEach(() => esArchiver.unload('actions/basic'));
|
||||
|
||||
it('should return 204 when deleting an action', async () => {
|
||||
await supertest
|
||||
.delete(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
});
|
||||
|
||||
it('should return 204 when deleting an action in a space', async () => {
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
});
|
||||
|
||||
it('should return 404 when deleting an action in another space', async () => {
|
||||
await supertest
|
||||
.delete(`/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it(`should return 404 when action doesn't exist`, async () => {
|
||||
await supertest
|
||||
.delete('/api/action/2')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(404)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [action/2] not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,293 +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 expect from '@kbn/expect';
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
|
||||
const esTestIndexName = '.kibaka-alerting-test-data';
|
||||
|
||||
describe('execute', () => {
|
||||
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: {
|
||||
mappings: {
|
||||
properties: {
|
||||
source: {
|
||||
type: 'keyword',
|
||||
},
|
||||
reference: {
|
||||
type: 'keyword',
|
||||
},
|
||||
params: {
|
||||
enabled: false,
|
||||
type: 'object',
|
||||
},
|
||||
config: {
|
||||
enabled: false,
|
||||
type: 'object',
|
||||
},
|
||||
state: {
|
||||
enabled: false,
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
after(() => es.indices.delete({ index: esTestIndexName }));
|
||||
|
||||
it('decrypts attributes when calling execute API', async () => {
|
||||
await supertest
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-1',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.be.an('object');
|
||||
});
|
||||
const indexedRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'action:test.index-record',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'actions-execute-1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(indexedRecord._source).to.eql({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-1',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
reference: 'actions-execute-1',
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
});
|
||||
|
||||
it(`can't execute from another space`, async () => {
|
||||
await supertest
|
||||
.post(`/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-2',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('execute works in a space', async () => {
|
||||
await supertest
|
||||
.post(`/s/space_1/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-3',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.be.an('object');
|
||||
});
|
||||
const indexedRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'action:test.index-record',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'actions-execute-3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(indexedRecord._source).to.eql({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-3',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
reference: 'actions-execute-3',
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
});
|
||||
|
||||
it('execute 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')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(updatedAction).to.eql({
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
await supertest
|
||||
.post(`/api/action/${ES_ARCHIVER_ACTION_ID}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-4',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.be.an('object');
|
||||
});
|
||||
const indexedRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'action:test.index-record',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'actions-execute-4',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(indexedRecord._source).to.eql({
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'actions-execute-4',
|
||||
message: 'Testing 123',
|
||||
},
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
reference: 'actions-execute-4',
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 404 when action doesn't exist`, async () => {
|
||||
const { body: response } = await supertest
|
||||
.post('/api/action/1/_execute')
|
||||
.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}/_execute`)
|
||||
.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'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,84 +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 expect from '@kbn/expect';
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function findActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('find', () => {
|
||||
before(() => esArchiver.load('actions/basic'));
|
||||
after(() => esArchiver.unload('actions/basic'));
|
||||
|
||||
it('should return 200 with individual responses', async () => {
|
||||
await supertest
|
||||
.get(
|
||||
'/api/action/_find?search=test.index-record&search_fields=actionTypeId&fields=description'
|
||||
)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
data: [
|
||||
{
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
description: 'My action',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 with individual responses in a space', async () => {
|
||||
await supertest
|
||||
.get(
|
||||
'/s/space_1/api/action/_find?search=test.index-record&search_fields=actionTypeId&fields=description'
|
||||
)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
data: [
|
||||
{
|
||||
id: SPACE_1_ES_ARCHIVER_ACTION_ID,
|
||||
description: 'My action',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return encrypted attributes', async () => {
|
||||
await supertest
|
||||
.get('/api/action/_find?search=test.index-record&search_fields=actionTypeId')
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
data: [
|
||||
{
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,68 +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 expect from '@kbn/expect';
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function getActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('get', () => {
|
||||
before(() => esArchiver.load('actions/basic'));
|
||||
after(() => esArchiver.unload('actions/basic'));
|
||||
|
||||
it('should return 200 when finding a record and not return encrypted attributes', async () => {
|
||||
await supertest
|
||||
.get(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when finding a record in another space', async () => {
|
||||
await supertest.get(`/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}`).expect(404);
|
||||
});
|
||||
|
||||
it('should return 200 when finding a record in a space', async () => {
|
||||
await supertest
|
||||
.get(`/s/space_1/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}`)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: SPACE_1_ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when not finding a record', async () => {
|
||||
await supertest
|
||||
.get('/api/action/2')
|
||||
.expect(404)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [action/2] not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,32 +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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function listActionTypesTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('list_action_types', () => {
|
||||
it('should return 200 with list of action types containing defaults', async () => {
|
||||
await supertest
|
||||
.get('/api/action/types')
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
function createActionTypeMatcher(id: string, name: string) {
|
||||
return (actionType: { id: string; name: string }) => {
|
||||
return actionType.id === id && actionType.name === name;
|
||||
};
|
||||
}
|
||||
// Check for values explicitly in order to avoid this test failing each time plugins register
|
||||
// a new action type
|
||||
expect(
|
||||
resp.body.some(createActionTypeMatcher('test.index-record', 'Test: Index Record'))
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,261 +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 expect from '@kbn/expect';
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function updateActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('update', () => {
|
||||
beforeEach(() => esArchiver.load('actions/basic'));
|
||||
afterEach(() => esArchiver.unload('actions/basic'));
|
||||
|
||||
it('should return 200 when updating a document', async () => {
|
||||
await supertest
|
||||
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when updating a document in a space', async () => {
|
||||
await supertest
|
||||
.put(`/s/space_1/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: SPACE_1_ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when updating a document in another space', async () => {
|
||||
await supertest
|
||||
.put(`/api/action/${SPACE_1_ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should not be able to pass null config', async () => {
|
||||
await supertest
|
||||
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: null,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "config" fails because ["config" must be an object]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['config'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return encrypted attributes', async () => {
|
||||
const { body: updatedAction } = await supertest
|
||||
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(updatedAction).to.eql({
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
const { body: fetchedAction } = await supertest
|
||||
.get(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.expect(200);
|
||||
expect(fetchedAction).to.eql({
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when updating a non existing document', async () => {
|
||||
await supertest
|
||||
.put('/api/action/2')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(404)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [action/2] not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when payload is empty and invalid', async () => {
|
||||
await supertest
|
||||
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "description" fails because ["description" is required]',
|
||||
validation: { source: 'payload', keys: ['description'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when secrets are not valid`, async () => {
|
||||
await supertest
|
||||
.put(`/api/action/${ES_ARCHIVER_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 42,
|
||||
},
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [encrypted]: expected value of type [string] but got [number]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should allow changing non-secret config properties - create`, async () => {
|
||||
let emailActionId: string = '';
|
||||
|
||||
// create the action
|
||||
await supertest
|
||||
.post('/api/action')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'test email action',
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
from: 'email-from@example.com',
|
||||
host: 'host-is-ignored-here.example.com',
|
||||
port: 666,
|
||||
},
|
||||
secrets: {
|
||||
user: 'email-user',
|
||||
password: 'email-password',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
emailActionId = resp.body.id;
|
||||
});
|
||||
|
||||
// add a new config param
|
||||
await supertest
|
||||
.put(`/api/action/${emailActionId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'a test email action 2',
|
||||
config: {
|
||||
from: 'email-from@example.com',
|
||||
service: '__json',
|
||||
},
|
||||
secrets: {
|
||||
user: 'email-user',
|
||||
password: 'email-password',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// execute the action
|
||||
await supertest
|
||||
.post(`/api/action/${emailActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
to: ['X'],
|
||||
subject: 'email-subject',
|
||||
message: 'email-message',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,325 +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 expect from '@kbn/expect';
|
||||
import { getTestAlertData, setupEsTestIndex, destroyEsTestIndex } from './utils';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
|
||||
export default function alertTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('alerts', () => {
|
||||
let esTestIndexName: string;
|
||||
const createdAlertIds: Array<{ space: string; id: string }> = [];
|
||||
|
||||
before(async () => {
|
||||
await destroyEsTestIndex(es);
|
||||
({ name: esTestIndexName } = await setupEsTestIndex(es));
|
||||
await esArchiver.load('actions/basic');
|
||||
});
|
||||
after(async () => {
|
||||
await Promise.all(
|
||||
createdAlertIds.map(({ space, id }) => {
|
||||
const urlPrefix = space !== 'default' ? `/s/${space}` : '';
|
||||
return supertest
|
||||
.delete(`${urlPrefix}/api/alert/${id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
})
|
||||
);
|
||||
await esArchiver.unload('actions/basic');
|
||||
await destroyEsTestIndex(es);
|
||||
});
|
||||
|
||||
it('should schedule task, run alert and fire actions', async () => {
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
interval: '1s',
|
||||
alertTypeId: 'test.always-firing',
|
||||
alertTypeParams: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-1',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-1',
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
createdAlertIds.push({ space: 'default', id: resp.body.id });
|
||||
});
|
||||
const alertTestRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'alert:test.always-firing',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'create-test-1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(alertTestRecord._source).to.eql({
|
||||
source: 'alert:test.always-firing',
|
||||
reference: 'create-test-1',
|
||||
state: {},
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-1',
|
||||
},
|
||||
});
|
||||
const actionTestRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'action:test.index-record',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'create-test-1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(actionTestRecord._source).to.eql({
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-1',
|
||||
message: 'instanceContextValue: true, instanceStateValue: true',
|
||||
},
|
||||
reference: 'create-test-1',
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
});
|
||||
|
||||
it('should schedule task, run alert and fire actions in a space', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
interval: '1s',
|
||||
alertTypeId: 'test.always-firing',
|
||||
alertTypeParams: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-2',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: SPACE_1_ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-2',
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
createdAlertIds.push({ space: 'space_1', id: createdAlert.id });
|
||||
|
||||
const alertTestRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'alert:test.always-firing',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'create-test-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(alertTestRecord._source).to.eql({
|
||||
source: 'alert:test.always-firing',
|
||||
reference: 'create-test-2',
|
||||
state: {},
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-2',
|
||||
},
|
||||
});
|
||||
const actionTestRecord = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source: 'action:test.index-record',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference: 'create-test-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(actionTestRecord._source).to.eql({
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-2',
|
||||
message: 'instanceContextValue: true, instanceStateValue: true',
|
||||
},
|
||||
reference: 'create-test-2',
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom retry logic', async () => {
|
||||
// We'll use this start time to query tasks created after this point
|
||||
const testStart = new Date();
|
||||
// We have to provide the test.rate-limit the next runAt, for testing purposes
|
||||
const retryDate = new Date(Date.now() + 60000);
|
||||
|
||||
const { body: createdAlert } = await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
interval: '1m',
|
||||
alertTypeId: 'test.always-firing',
|
||||
alertTypeParams: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-2',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: 'ce37997f-0fb6-460a-8baf-f81ac5d38348',
|
||||
params: {
|
||||
index: esTestIndexName,
|
||||
reference: 'create-test-1',
|
||||
retryAt: retryDate.getTime(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
createdAlertIds.push({ space: 'default', id: createdAlert.id });
|
||||
|
||||
const scheduledActionTask = await retry.tryForTime(15000, async () => {
|
||||
const searchResult = await es.search({
|
||||
index: '.kibana_task_manager',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'task.status': 'idle',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'task.attempts': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'task.taskType': 'actions:test.rate-limit',
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'task.scheduledAt': {
|
||||
gte: testStart,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
});
|
||||
expect(scheduledActionTask._source.task.runAt).to.eql(retryDate.toISOString());
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,13 +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 {
|
||||
ES_ARCHIVER_ACTION_ID as ActionArchiverActionId,
|
||||
SPACE_1_ES_ARCHIVER_ACTION_ID as ActionArchiverSpace1ActionId,
|
||||
} from '../actions/constants';
|
||||
|
||||
export const ES_ARCHIVER_ACTION_ID = ActionArchiverActionId;
|
||||
export const SPACE_1_ES_ARCHIVER_ACTION_ID = ActionArchiverSpace1ActionId;
|
|
@ -1,217 +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 expect from '@kbn/expect';
|
||||
import { getTestAlertData } from './utils';
|
||||
import { ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createAlertTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
describe('create', () => {
|
||||
const createdAlertIds: Array<{ space: string; id: string }> = [];
|
||||
|
||||
before(() => esArchiver.load('actions/basic'));
|
||||
after(async () => {
|
||||
await Promise.all(
|
||||
createdAlertIds.map(({ space, id }) => {
|
||||
const urlPrefix = space !== 'default' ? `/s/${space}` : '';
|
||||
return supertest
|
||||
.delete(`${urlPrefix}/api/alert/${id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
})
|
||||
);
|
||||
await esArchiver.unload('actions/basic');
|
||||
});
|
||||
|
||||
async function getScheduledTask(id: string) {
|
||||
return await es.get({
|
||||
id: `task:${id}`,
|
||||
index: '.kibana_task_manager',
|
||||
});
|
||||
}
|
||||
|
||||
it('should return 200 when creating an alert', async () => {
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then(async (resp: any) => {
|
||||
createdAlertIds.push({ space: 'default', id: resp.body.id });
|
||||
expect(resp.body).to.eql({
|
||||
id: resp.body.id,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
alertTypeId: 'test.noop',
|
||||
alertTypeParams: {},
|
||||
interval: '10s',
|
||||
scheduledTaskId: resp.body.scheduledTaskId,
|
||||
});
|
||||
expect(typeof resp.body.scheduledTaskId).to.be('string');
|
||||
const { _source: taskRecord } = await getScheduledTask(resp.body.scheduledTaskId);
|
||||
expect(taskRecord.type).to.eql('task');
|
||||
expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
|
||||
expect(JSON.parse(taskRecord.task.params)).to.eql({
|
||||
alertId: resp.body.id,
|
||||
spaceId: 'default',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when creating an alert in a space', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200);
|
||||
createdAlertIds.push({ space: 'space_1', id: createdAlert.id });
|
||||
expect(createdAlert).to.eql({
|
||||
id: createdAlert.id,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
enabled: true,
|
||||
alertTypeId: 'test.noop',
|
||||
alertTypeParams: {},
|
||||
interval: '10s',
|
||||
scheduledTaskId: createdAlert.scheduledTaskId,
|
||||
});
|
||||
expect(typeof createdAlert.scheduledTaskId).to.be('string');
|
||||
const { _source: taskRecord } = await getScheduledTask(createdAlert.scheduledTaskId);
|
||||
expect(taskRecord.type).to.eql('task');
|
||||
expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
|
||||
expect(JSON.parse(taskRecord.task.params)).to.eql({
|
||||
alertId: createdAlert.id,
|
||||
spaceId: 'space_1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not schedule a task when creating a disabled alert', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ enabled: false }))
|
||||
.expect(200);
|
||||
expect(createdAlert.scheduledTaskId).to.eql(undefined);
|
||||
});
|
||||
|
||||
it(`should return 400 when alert type isn't registered`, async () => {
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.unregistered-alert-type',
|
||||
})
|
||||
)
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Alert type "test.unregistered-alert-type" is not registered.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when payload is empty and invalid', async () => {
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "alertTypeId" fails because ["alertTypeId" is required]. child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['alertTypeId', 'interval', 'alertTypeParams', 'actions'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when alertTypeParams isn't valid`, async () => {
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.validation',
|
||||
})
|
||||
)
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when interval is wrong syntax`, async () => {
|
||||
const { body: error } = await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ interval: '10x' }))
|
||||
.expect(400);
|
||||
expect(error).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "interval" fails because ["interval" with value "10x" fails to match the seconds (5s) pattern, "interval" with value "10x" fails to match the minutes (5m) pattern, "interval" with value "10x" fails to match the hours (5h) pattern, "interval" with value "10x" fails to match the days (5d) pattern]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['interval', 'interval', 'interval', 'interval'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when interval is 0`, async () => {
|
||||
const { body: error } = await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ interval: '0s' }))
|
||||
.expect(400);
|
||||
expect(error).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "interval" fails because ["interval" with value "0s" fails to match the seconds (5s) pattern, "interval" with value "0s" fails to match the minutes (5m) pattern, "interval" with value "0s" fails to match the hours (5h) pattern, "interval" with value "0s" fails to match the days (5d) pattern]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['interval', 'interval', 'interval', 'interval'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,89 +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 expect from '@kbn/expect';
|
||||
import { getTestAlertData } from './utils';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createDeleteTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
describe('delete', () => {
|
||||
let alertId: string;
|
||||
let scheduledTaskId: string;
|
||||
let space1AlertId: string;
|
||||
let space1ScheduledTaskId: string;
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('actions/basic');
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
alertId = resp.body.id;
|
||||
scheduledTaskId = resp.body.scheduledTaskId;
|
||||
});
|
||||
await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
space1AlertId = resp.body.id;
|
||||
space1ScheduledTaskId = resp.body.scheduledTaskId;
|
||||
});
|
||||
});
|
||||
after(() => esArchiver.unload('actions/basic'));
|
||||
|
||||
async function getScheduledTask(id: string) {
|
||||
return await es.get({
|
||||
id,
|
||||
index: '.kibana_task_manager',
|
||||
});
|
||||
}
|
||||
|
||||
it('should return 204 when deleting an alert and removing scheduled task', async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
let hasThrownError = false;
|
||||
try {
|
||||
await getScheduledTask(scheduledTaskId);
|
||||
} catch (e) {
|
||||
hasThrownError = true;
|
||||
expect(e.status).to.eql(404);
|
||||
}
|
||||
expect(hasThrownError).to.eql(true);
|
||||
});
|
||||
|
||||
it('should return 404 when deleting an alert from another space', async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 204 when deleting an alert in a space', async () => {
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
let hasThrownError = false;
|
||||
try {
|
||||
await getScheduledTask(space1ScheduledTaskId);
|
||||
} catch (e) {
|
||||
hasThrownError = true;
|
||||
expect(e.status).to.eql(404);
|
||||
}
|
||||
expect(hasThrownError).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,71 +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 { getTestAlertData } from './utils';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createDisableAlertTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('disable', () => {
|
||||
let alertId: string;
|
||||
let space1AlertId: string;
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('actions/basic');
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ enabled: true }))
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
alertId = resp.body.id;
|
||||
});
|
||||
await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ enabled: true }))
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
space1AlertId = resp.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await esArchiver.unload('actions/basic');
|
||||
});
|
||||
|
||||
it('should return 204 when disabling an alert', async () => {
|
||||
await supertest
|
||||
.post(`/api/alert/${alertId}/_disable`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it('should return 404 when disabling an alert from another space', async () => {
|
||||
await supertest
|
||||
.post(`/api/alert/${space1AlertId}/_disable`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 204 when disabling an alert in a space', async () => {
|
||||
await supertest
|
||||
.post(`/s/space_1/api/alert/${space1AlertId}/_disable`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,71 +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 { getTestAlertData } from './utils';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createEnableAlertTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('enable', () => {
|
||||
let alertId: string;
|
||||
let space1AlertId: string;
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('actions/basic');
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ enabled: false }))
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
alertId = resp.body.id;
|
||||
});
|
||||
await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ enabled: false }))
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
space1AlertId = resp.body.id;
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await esArchiver.unload('actions/basic');
|
||||
});
|
||||
|
||||
it('should return 204 when enabling an alert', async () => {
|
||||
await supertest
|
||||
.post(`/api/alert/${alertId}/_enable`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
});
|
||||
|
||||
it('should return 404 when enabling an alert from another space', async () => {
|
||||
await supertest
|
||||
.post(`/api/alert/${space1AlertId}/_enable`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 204 when enabling an alert in a space', async () => {
|
||||
await supertest
|
||||
.post(`/s/space_1/api/alert/${space1AlertId}/_enable`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,109 +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 expect from '@kbn/expect';
|
||||
import { getTestAlertData } from './utils';
|
||||
import { ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createFindTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('find', () => {
|
||||
let alertId: string;
|
||||
let space1AlertId: string;
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('actions/basic');
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
alertId = resp.body.id;
|
||||
});
|
||||
await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
space1AlertId = resp.body.id;
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await esArchiver.unload('actions/basic');
|
||||
});
|
||||
|
||||
it('should return 200 when finding alerts', async () => {
|
||||
await supertest
|
||||
.get('/api/alert/_find')
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
const body = resp.body;
|
||||
expect(body.page).to.equal(1);
|
||||
expect(body.perPage).to.be.greaterThan(0);
|
||||
expect(body.total).to.be.greaterThan(0);
|
||||
const match = body.data.find((obj: any) => obj.id === alertId);
|
||||
expect(match).to.eql({
|
||||
id: alertId,
|
||||
alertTypeId: 'test.noop',
|
||||
interval: '10s',
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
alertTypeParams: {},
|
||||
scheduledTaskId: match.scheduledTaskId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 when finding alerts in a space', async () => {
|
||||
await supertest
|
||||
.get('/s/space_1/api/alert/_find')
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
const match = resp.body.data.find((obj: any) => obj.id === space1AlertId);
|
||||
expect(match).to.eql({
|
||||
id: space1AlertId,
|
||||
alertTypeId: 'test.noop',
|
||||
interval: '10s',
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
alertTypeParams: {},
|
||||
scheduledTaskId: match.scheduledTaskId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,107 +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 expect from '@kbn/expect';
|
||||
import { getTestAlertData } from './utils';
|
||||
import { ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createGetTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('get', () => {
|
||||
let alertId: string;
|
||||
let space1AlertId: string;
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('actions/basic');
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
alertId = resp.body.id;
|
||||
});
|
||||
await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
space1AlertId = resp.body.id;
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await esArchiver.unload('actions/basic');
|
||||
});
|
||||
|
||||
it('should return 200 when getting an alert', async () => {
|
||||
await supertest
|
||||
.get(`/api/alert/${alertId}`)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: alertId,
|
||||
alertTypeId: 'test.noop',
|
||||
interval: '10s',
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
alertTypeParams: {},
|
||||
scheduledTaskId: resp.body.scheduledTaskId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when gettin an alert in another space', async () => {
|
||||
await supertest.get(`/api/alert/${space1AlertId}`).expect(404);
|
||||
});
|
||||
|
||||
it('should return 200 when getting an alert in a space', async () => {
|
||||
await supertest
|
||||
.get(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
id: space1AlertId,
|
||||
alertTypeId: 'test.noop',
|
||||
interval: '10s',
|
||||
enabled: true,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
alertTypeParams: {},
|
||||
scheduledTaskId: resp.body.scheduledTaskId,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,27 +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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function listAlertTypes({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('list_alert_types', () => {
|
||||
it('should return 200 with list of alert types', async () => {
|
||||
await supertest
|
||||
.get('/api/alert/types')
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
const fixtureAlertType = resp.body.find((alertType: any) => alertType.id === 'test.noop');
|
||||
expect(fixtureAlertType).to.eql({
|
||||
id: 'test.noop',
|
||||
name: 'Test: Noop',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,239 +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 expect from '@kbn/expect';
|
||||
import { getTestAlertData } from './utils';
|
||||
import { ES_ARCHIVER_ACTION_ID, SPACE_1_ES_ARCHIVER_ACTION_ID } from './constants';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function createUpdateTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('update', () => {
|
||||
let alertId: string;
|
||||
let space1AlertId: string;
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('actions/basic');
|
||||
await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
alertId = resp.body.id;
|
||||
});
|
||||
await supertest
|
||||
.post('/s/space_1/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData())
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
space1AlertId = resp.body.id;
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await supertest
|
||||
.delete(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204, '');
|
||||
await esArchiver.unload('actions/basic');
|
||||
});
|
||||
|
||||
it('should return 200 when updating an alert', async () => {
|
||||
const alert = {
|
||||
alertTypeParams: {
|
||||
foo: true,
|
||||
},
|
||||
interval: '12s',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'UPDATED: instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await supertest
|
||||
.put(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(alert)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
...alert,
|
||||
id: alertId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 when updating an alert from another space', async () => {
|
||||
await supertest
|
||||
.put(`/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
alertTypeParams: {
|
||||
foo: true,
|
||||
},
|
||||
interval: '12s',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'UPDATED: instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 200 when updating an alert in a space', async () => {
|
||||
const alert = {
|
||||
alertTypeParams: {
|
||||
foo: true,
|
||||
},
|
||||
interval: '12s',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: SPACE_1_ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'UPDATED: instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await supertest
|
||||
.put(`/s/space_1/api/alert/${space1AlertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(alert)
|
||||
.expect(200)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
...alert,
|
||||
id: space1AlertId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when attempting to change alert type', async () => {
|
||||
await supertest
|
||||
.put(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
alertTypeId: '1',
|
||||
alertTypeParams: {
|
||||
foo: true,
|
||||
},
|
||||
interval: '12s',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: ES_ARCHIVER_ACTION_ID,
|
||||
params: {
|
||||
message:
|
||||
'UPDATED: instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: '"alertTypeId" is not allowed',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['alertTypeId'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 when payload is empty and invalid', async () => {
|
||||
await supertest
|
||||
.put(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "interval" fails because ["interval" is required]. child "alertTypeParams" fails because ["alertTypeParams" is required]. child "actions" fails because ["actions" is required]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['interval', 'alertTypeParams', 'actions'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when alertTypeConfig isn't valid`, async () => {
|
||||
const { body: customAlert } = await supertest
|
||||
.post('/api/alert')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.validation',
|
||||
alertTypeParams: {
|
||||
param1: 'test',
|
||||
},
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
await supertest
|
||||
.put(`/api/alert/${customAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
interval: '10s',
|
||||
alertTypeParams: {},
|
||||
actions: [],
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return 400 when interval is wrong syntax`, async () => {
|
||||
const { body: error } = await supertest
|
||||
.put(`/api/alert/${alertId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestAlertData({ interval: '10x', enabled: undefined }))
|
||||
.expect(400);
|
||||
expect(error).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "interval" fails because ["interval" with value "10x" fails to match the seconds pattern, "interval" with value "10x" fails to match the minutes pattern, "interval" with value "10x" fails to match the hours pattern, "interval" with value "10x" fails to match the days pattern]. "alertTypeId" is not allowed',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['interval', 'interval', 'interval', 'interval', 'alertTypeId'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
70
x-pack/test/alerting_api_integration/common/config.ts
Normal file
70
x-pack/test/alerting_api_integration/common/config.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 path from 'path';
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
import { services } from './services';
|
||||
import { SLACK_ACTION_SIMULATOR_URI } from './fixtures/plugins/actions';
|
||||
|
||||
interface CreateTestConfigOptions {
|
||||
license: string;
|
||||
disabledPlugins?: string[];
|
||||
ssl?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
|
||||
const { license = 'trial', disabledPlugins = [], ssl = false } = options;
|
||||
|
||||
return async ({ readConfigFile }: FtrConfigProviderContext) => {
|
||||
const xPackApiIntegrationTestsConfig = await readConfigFile(
|
||||
require.resolve('../../api_integration/config.js')
|
||||
);
|
||||
const servers = {
|
||||
...xPackApiIntegrationTestsConfig.get('servers'),
|
||||
elasticsearch: {
|
||||
...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'),
|
||||
protocol: ssl ? 'https' : 'http',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve(`../${name}/tests/`)],
|
||||
servers,
|
||||
services,
|
||||
junit: {
|
||||
reportName: 'X-Pack Alerting API Integration Tests',
|
||||
},
|
||||
esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'),
|
||||
esTestCluster: {
|
||||
...xPackApiIntegrationTestsConfig.get('esTestCluster'),
|
||||
license,
|
||||
ssl,
|
||||
serverArgs: [
|
||||
`xpack.license.self_generated.type=${license}`,
|
||||
`xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`,
|
||||
],
|
||||
},
|
||||
kbnTestServer: {
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,
|
||||
`--server.xsrf.whitelist=${JSON.stringify([SLACK_ACTION_SIMULATOR_URI])}`,
|
||||
...(ssl
|
||||
? [
|
||||
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { AlertExecutorOptions, AlertType } from '../../../../../legacy/plugins/alerting';
|
||||
import { ActionTypeExecutorOptions, ActionType } from '../../../../../legacy/plugins/actions';
|
||||
import { AlertExecutorOptions, AlertType } from '../../../../../../legacy/plugins/alerting';
|
||||
import { ActionTypeExecutorOptions, ActionType } from '../../../../../../legacy/plugins/actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(kibana: any) {
|
||||
|
@ -97,9 +97,71 @@ export default function(kibana: any) {
|
|||
};
|
||||
},
|
||||
};
|
||||
const authorizationActionType: ActionType = {
|
||||
id: 'test.authorization',
|
||||
name: 'Test: Authorization',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
callClusterAuthorizationIndex: schema.string(),
|
||||
savedObjectsClientType: schema.string(),
|
||||
savedObjectsClientId: schema.string(),
|
||||
index: schema.string(),
|
||||
reference: schema.string(),
|
||||
}),
|
||||
},
|
||||
async executor({ params, services }: ActionTypeExecutorOptions) {
|
||||
// Call cluster
|
||||
let callClusterSuccess = false;
|
||||
let callClusterError;
|
||||
try {
|
||||
await services.callCluster('index', {
|
||||
index: params.callClusterAuthorizationIndex,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
param1: 'test',
|
||||
},
|
||||
});
|
||||
callClusterSuccess = true;
|
||||
} catch (e) {
|
||||
callClusterError = e;
|
||||
}
|
||||
// Saved objects client
|
||||
let savedObjectsClientSuccess = false;
|
||||
let savedObjectsClientError;
|
||||
try {
|
||||
await services.savedObjectsClient.get(
|
||||
params.savedObjectsClientType,
|
||||
params.savedObjectsClientId
|
||||
);
|
||||
savedObjectsClientSuccess = true;
|
||||
} catch (e) {
|
||||
savedObjectsClientError = e;
|
||||
}
|
||||
// Save the result
|
||||
await services.callCluster('index', {
|
||||
index: params.index,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
state: {
|
||||
callClusterSuccess,
|
||||
callClusterError,
|
||||
savedObjectsClientSuccess,
|
||||
savedObjectsClientError,
|
||||
},
|
||||
params,
|
||||
reference: params.reference,
|
||||
source: 'action:test.authorization',
|
||||
},
|
||||
});
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
},
|
||||
};
|
||||
server.plugins.actions.registerType(indexRecordActionType);
|
||||
server.plugins.actions.registerType(failingActionType);
|
||||
server.plugins.actions.registerType(rateLimitedActionType);
|
||||
server.plugins.actions.registerType(authorizationActionType);
|
||||
|
||||
// Alert types
|
||||
const alwaysFiringAlertType: AlertType = {
|
||||
|
@ -164,6 +226,64 @@ export default function(kibana: any) {
|
|||
throw new Error('Failed to execute alert type');
|
||||
},
|
||||
};
|
||||
const authorizationAlertType: AlertType = {
|
||||
id: 'test.authorization',
|
||||
name: 'Test: Authorization',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
callClusterAuthorizationIndex: schema.string(),
|
||||
savedObjectsClientType: schema.string(),
|
||||
savedObjectsClientId: schema.string(),
|
||||
index: schema.string(),
|
||||
reference: schema.string(),
|
||||
}),
|
||||
},
|
||||
async executor({ services, params, state }: AlertExecutorOptions) {
|
||||
// Call cluster
|
||||
let callClusterSuccess = false;
|
||||
let callClusterError;
|
||||
try {
|
||||
await services.callCluster('index', {
|
||||
index: params.callClusterAuthorizationIndex,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
param1: 'test',
|
||||
},
|
||||
});
|
||||
callClusterSuccess = true;
|
||||
} catch (e) {
|
||||
callClusterError = e;
|
||||
}
|
||||
// Saved objects client
|
||||
let savedObjectsClientSuccess = false;
|
||||
let savedObjectsClientError;
|
||||
try {
|
||||
await services.savedObjectsClient.get(
|
||||
params.savedObjectsClientType,
|
||||
params.savedObjectsClientId
|
||||
);
|
||||
savedObjectsClientSuccess = true;
|
||||
} catch (e) {
|
||||
savedObjectsClientError = e;
|
||||
}
|
||||
// Save the result
|
||||
await services.callCluster('index', {
|
||||
index: params.index,
|
||||
refresh: 'wait_for',
|
||||
body: {
|
||||
state: {
|
||||
callClusterSuccess,
|
||||
callClusterError,
|
||||
savedObjectsClientSuccess,
|
||||
savedObjectsClientError,
|
||||
},
|
||||
params,
|
||||
reference: params.reference,
|
||||
source: 'alert:test.authorization',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
const validationAlertType: AlertType = {
|
||||
id: 'test.validation',
|
||||
name: 'Test: Validation',
|
||||
|
@ -183,6 +303,7 @@ export default function(kibana: any) {
|
|||
server.plugins.alerting.registerType(neverFiringAlertType);
|
||||
server.plugins.alerting.registerType(failingAlertType);
|
||||
server.plugins.alerting.registerType(validationAlertType);
|
||||
server.plugins.alerting.registerType(authorizationAlertType);
|
||||
server.plugins.alerting.registerType(noopAlertType);
|
||||
},
|
||||
});
|
|
@ -4,5 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const ES_ARCHIVER_ACTION_ID = '60071910-1ef1-4a72-9a68-b94af6a836a7';
|
||||
export const SPACE_1_ES_ARCHIVER_ACTION_ID = '6c7d0f6b-2fb5-4821-b182-624fc3ccc7a3';
|
||||
export { ObjectRemover } from './object_remover';
|
||||
export { getUrlPrefix } from './space_test_utils';
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { getUrlPrefix } from './space_test_utils';
|
||||
|
||||
interface ObjectToRemove {
|
||||
spaceId: string;
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class ObjectRemover {
|
||||
private readonly supertest: any;
|
||||
private objectsToRemove: ObjectToRemove[] = [];
|
||||
|
||||
constructor(supertest: any) {
|
||||
this.supertest = supertest;
|
||||
}
|
||||
|
||||
add(spaceId: ObjectToRemove['spaceId'], id: ObjectToRemove['id'], type: ObjectToRemove['type']) {
|
||||
this.objectsToRemove.push({ spaceId, id, type });
|
||||
}
|
||||
|
||||
async removeAll() {
|
||||
await Promise.all(
|
||||
this.objectsToRemove.map(({ spaceId, id, type }) => {
|
||||
return this.supertest
|
||||
.delete(`${getUrlPrefix(spaceId)}/api/${type}/${id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
})
|
||||
);
|
||||
this.objectsToRemove = [];
|
||||
}
|
||||
}
|
|
@ -4,11 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('apis', function () {
|
||||
this.tags('ciGroup8');
|
||||
|
||||
loadTestFile(require.resolve('./actions'));
|
||||
loadTestFile(require.resolve('./alerting'));
|
||||
});
|
||||
export function getUrlPrefix(spaceId: string) {
|
||||
return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``;
|
||||
}
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { services } from '../api_integration/services';
|
||||
export { services } from '../../api_integration/services';
|
57
x-pack/test/alerting_api_integration/common/types.ts
Normal file
57
x-pack/test/alerting_api_integration/common/types.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
interface FeaturesPrivileges {
|
||||
[featureId: string]: string[];
|
||||
}
|
||||
|
||||
// TODO: Consolidate the following type definitions
|
||||
interface CustomRoleSpecificationElasticsearchIndices {
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
}
|
||||
|
||||
export interface RoleKibanaPrivilege {
|
||||
spaces: string[];
|
||||
base?: string[];
|
||||
feature?: FeaturesPrivileges;
|
||||
}
|
||||
|
||||
export interface CustomRoleSpecification {
|
||||
name: string;
|
||||
elasticsearch?: {
|
||||
cluster?: string[];
|
||||
indices?: CustomRoleSpecificationElasticsearchIndices[];
|
||||
};
|
||||
kibana?: RoleKibanaPrivilege[];
|
||||
}
|
||||
|
||||
interface ReservedRoleSpecification {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function isCustomRoleSpecification(
|
||||
roleSpecification: CustomRoleSpecification | ReservedRoleSpecification
|
||||
): roleSpecification is CustomRoleSpecification {
|
||||
const customRoleDefinition = roleSpecification as CustomRoleSpecification;
|
||||
return (
|
||||
customRoleDefinition.kibana !== undefined || customRoleDefinition.elasticsearch !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
fullName: string;
|
||||
password: string;
|
||||
role?: ReservedRoleSpecification | CustomRoleSpecification;
|
||||
roles?: Array<ReservedRoleSpecification | CustomRoleSpecification>;
|
||||
}
|
||||
|
||||
export interface Space {
|
||||
id: string;
|
||||
name: string;
|
||||
disabledFeatures: string[];
|
||||
}
|
|
@ -1,51 +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 { join, resolve } from 'path';
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { services } from './services';
|
||||
import { SLACK_ACTION_SIMULATOR_URI } from './fixtures/plugins/actions';
|
||||
|
||||
export async function getApiIntegrationConfig({ readConfigFile }) {
|
||||
const xPackApiIntegrationTestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
|
||||
|
||||
const servers = {
|
||||
...xPackApiIntegrationTestsConfig.get('servers'),
|
||||
elasticsearch: {
|
||||
...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'),
|
||||
protocol: 'https',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('./apis')],
|
||||
services,
|
||||
servers,
|
||||
esArchiver: {
|
||||
directory: resolve(__dirname, 'es_archives'),
|
||||
},
|
||||
junit: {
|
||||
reportName: 'X-Pack Alerting API Integration Tests',
|
||||
},
|
||||
kbnTestServer: {
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
`--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
|
||||
`--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'actions')}`,
|
||||
`--server.xsrf.whitelist=${JSON.stringify([SLACK_ACTION_SIMULATOR_URI])}`,
|
||||
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
],
|
||||
},
|
||||
esTestCluster: {
|
||||
...xPackApiIntegrationTestsConfig.get('esTestCluster'),
|
||||
ssl: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default getApiIntegrationConfig;
|
|
@ -1,24 +0,0 @@
|
|||
The values of `id` and `secrets` in the `basic/data.json` file
|
||||
may change over time, and to get the current "correct" value to replace it with,
|
||||
you can do the following:
|
||||
|
||||
|
||||
- add a `process.exit()` in this test, after an action is created:
|
||||
|
||||
https://github.com/elastic/kibana/blob/master/x-pack/test/api_integration/apis/actions/create.ts#L37
|
||||
|
||||
- figure out what data got put in ES via
|
||||
|
||||
curl -v 'http://elastic:changeme@localhost:9220/_search?q=type:action' | json
|
||||
|
||||
- there should be a new `id` and `secrets`
|
||||
|
||||
- update the following files:
|
||||
|
||||
- `id` and `secrets`
|
||||
|
||||
`x-pack/test/functional/es_archives/actions/basic/data.json`
|
||||
|
||||
- `id`
|
||||
|
||||
`x-pack/test/api_integration/apis/actions/constants.ts`
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"value": {
|
||||
"id": "action:60071910-1ef1-4a72-9a68-b94af6a836a7",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "action",
|
||||
"migrationVersion": {},
|
||||
"action": {
|
||||
"description": "My action",
|
||||
"actionTypeId": "test.index-record",
|
||||
"config": {
|
||||
"unencrypted" : "This value shouldn't get encrypted"
|
||||
},
|
||||
"secrets": "P1k6WTkcODQcb9+xWwO9s9qrCTimK2pMJDOtN0gJsraa0zx5dRroZ2Ppsr6D5ffPSN5QpO0aPVvef7/TLTw3A2Fn7Q44hrz+nEqZ+NBQ2zmQFq+8208DrhNu/Z+4M175UW+u8mUX3tkGqGU/rcbw9HGHK8NcpZC6Fgx5wCmUSfQWklNN2FJXjyAG"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"value": {
|
||||
"id": "action:08cca6da-60ed-49ca-85f6-641240300a3f",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "action",
|
||||
"migrationVersion": {},
|
||||
"action": {
|
||||
"description": "My failing action",
|
||||
"actionTypeId": "test.failing",
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"value": {
|
||||
"id": "action:ce37997f-0fb6-460a-8baf-f81ac5d38348",
|
||||
"index": ".kibana",
|
||||
"source": {
|
||||
"type": "action",
|
||||
"migrationVersion": {},
|
||||
"action": {
|
||||
"description": "My rate limited action",
|
||||
"actionTypeId": "test.rate-limit",
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"value": {
|
||||
"id" : "space_1:action:6c7d0f6b-2fb5-4821-b182-624fc3ccc7a3",
|
||||
"index" : ".kibana",
|
||||
"source" : {
|
||||
"action" : {
|
||||
"description" : "My action",
|
||||
"actionTypeId" : "test.index-record",
|
||||
"config" : {
|
||||
"unencrypted" : "This value shouldn't get encrypted"
|
||||
},
|
||||
"secrets" : "LEepwqGVUSkkAZSYCGDz3Y0DGoRgBGPRnu+Ta0jmEz+TiGUj3SBYWL6t2jqncBhsWgzSzwCusY+z5B/k+4wjaXW5t/KxNZP7bGpLQK0hL9IwKxqmRzRbEvX0nzeExxgfSaMRjjn2SnrE21MTw6qyBwGNqnuvmN7ILde4ZUbR9Jyjl8A6Y0GDcWvN"
|
||||
},
|
||||
"type": "action",
|
||||
"namespace": "space_1"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_shards": "1",
|
||||
"auto_expand_replicas": "0-1",
|
||||
"number_of_replicas": "0"
|
||||
}
|
||||
},
|
||||
"mappings": {
|
||||
"doc": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"spaceId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"migrationVersion": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"index-pattern": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 256
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"actionTypeId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"config": {
|
||||
"enabled": "false",
|
||||
"type": "object"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "binary"
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"properties": {
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"disabledFeatures": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"initials": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "text",
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 2048
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,284 +0,0 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"config": {
|
||||
"dynamic": "true",
|
||||
"properties": {
|
||||
"buildNum": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"dateFormat:tz": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 256,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"panelsJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"refreshInterval": {
|
||||
"properties": {
|
||||
"display": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"pause": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"section": {
|
||||
"type": "integer"
|
||||
},
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeFrom": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timeRestore": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"timeTo": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"index-pattern": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"fieldFormatMap": {
|
||||
"type": "text"
|
||||
},
|
||||
"fields": {
|
||||
"type": "text"
|
||||
},
|
||||
"intervalName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"notExpandable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sourceFilters": {
|
||||
"type": "text"
|
||||
},
|
||||
"timeFieldName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"properties": {
|
||||
"_reserved": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"disabledFeatures": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"initials": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spaceId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion-sheet": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"hits": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timelion_chart_height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_columns": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_other_interval": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"timelion_rows": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timelion_sheet": {
|
||||
"type": "text"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"url": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"accessCount": {
|
||||
"type": "long"
|
||||
},
|
||||
"accessDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"url": {
|
||||
"fields": {
|
||||
"keyword": {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword"
|
||||
}
|
||||
},
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualization": {
|
||||
"dynamic": "strict",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {
|
||||
"searchSourceJSON": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"uiStateJSON": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"visState": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"index": {
|
||||
"number_of_replicas": "1",
|
||||
"number_of_shards": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { createTestConfig } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default createTestConfig('security_and_spaces', {
|
||||
disabledPlugins: [],
|
||||
license: 'trial',
|
||||
ssl: true,
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* 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 { Space, User } from '../common/types';
|
||||
|
||||
const NoKibanaPrivileges: User = {
|
||||
username: 'no_kibana_privileges',
|
||||
fullName: 'no_kibana_privileges',
|
||||
password: 'no_kibana_privileges-password',
|
||||
role: {
|
||||
name: 'no_kibana_privileges',
|
||||
elasticsearch: {
|
||||
// TODO: Remove once Elasticsearch doesn't require the permission for own keys
|
||||
cluster: ['manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['foo'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
{
|
||||
names: ['.kibaka-alerting-test-data*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Superuser: User = {
|
||||
username: 'superuser',
|
||||
fullName: 'superuser',
|
||||
password: 'superuser-password',
|
||||
role: {
|
||||
name: 'superuser',
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalRead: User = {
|
||||
username: 'global_read',
|
||||
fullName: 'global_read',
|
||||
password: 'global_read-password',
|
||||
role: {
|
||||
name: 'global_read_role',
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
alerting: ['read'],
|
||||
actions: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
elasticsearch: {
|
||||
// TODO: Remove once Elasticsearch doesn't require the permission for own keys
|
||||
cluster: ['manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['.kibaka-alerting-test-data*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Space1All: User = {
|
||||
username: 'space_1_all',
|
||||
fullName: 'space_1_all',
|
||||
password: 'space_1_all-password',
|
||||
role: {
|
||||
name: 'space_1_all_role',
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
alerting: ['all'],
|
||||
actions: ['all'],
|
||||
},
|
||||
spaces: ['space1'],
|
||||
},
|
||||
],
|
||||
elasticsearch: {
|
||||
// TODO: Remove once Elasticsearch doesn't require the permission for own keys
|
||||
cluster: ['manage_api_key'],
|
||||
indices: [
|
||||
{
|
||||
names: ['.kibaka-alerting-test-data*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Users: User[] = [NoKibanaPrivileges, Superuser, GlobalRead, Space1All];
|
||||
|
||||
const Space1: Space = {
|
||||
id: 'space1',
|
||||
name: 'Space 1',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
const Space2: Space = {
|
||||
id: 'space2',
|
||||
name: 'Space 2',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
|
||||
export const Spaces: Space[] = [Space1, Space2];
|
||||
|
||||
// For all scenarios, we define both an instance in addition
|
||||
// to a "type" definition so that we can use the exhaustive switch in
|
||||
// typescript to ensure all scenarios are handled.
|
||||
|
||||
interface Scenario {
|
||||
user: User;
|
||||
space: Space;
|
||||
}
|
||||
|
||||
interface NoKibanaPrivilegesAtSpace1 extends Scenario {
|
||||
id: 'no_kibana_privileges at space1';
|
||||
}
|
||||
const NoKibanaPrivilegesAtSpace1: NoKibanaPrivilegesAtSpace1 = {
|
||||
id: 'no_kibana_privileges at space1',
|
||||
user: NoKibanaPrivileges,
|
||||
space: Space1,
|
||||
};
|
||||
|
||||
interface SuperuserAtSpace1 extends Scenario {
|
||||
id: 'superuser at space1';
|
||||
}
|
||||
const SuperuserAtSpace1: SuperuserAtSpace1 = {
|
||||
id: 'superuser at space1',
|
||||
user: Superuser,
|
||||
space: Space1,
|
||||
};
|
||||
|
||||
interface GlobalReadAtSpace1 extends Scenario {
|
||||
id: 'global_read at space1';
|
||||
}
|
||||
const GlobalReadAtSpace1: GlobalReadAtSpace1 = {
|
||||
id: 'global_read at space1',
|
||||
user: GlobalRead,
|
||||
space: Space1,
|
||||
};
|
||||
|
||||
interface Space1AllAtSpace1 extends Scenario {
|
||||
id: 'space_1_all at space1';
|
||||
}
|
||||
const Space1AllAtSpace1: Space1AllAtSpace1 = {
|
||||
id: 'space_1_all at space1',
|
||||
user: Space1All,
|
||||
space: Space1,
|
||||
};
|
||||
|
||||
interface Space1AllAtSpace2 extends Scenario {
|
||||
id: 'space_1_all at space2';
|
||||
}
|
||||
const Space1AllAtSpace2: Space1AllAtSpace2 = {
|
||||
id: 'space_1_all at space2',
|
||||
user: Space1All,
|
||||
space: Space2,
|
||||
};
|
||||
|
||||
export const UserAtSpaceScenarios: [
|
||||
NoKibanaPrivilegesAtSpace1,
|
||||
SuperuserAtSpace1,
|
||||
GlobalReadAtSpace1,
|
||||
Space1AllAtSpace1,
|
||||
Space1AllAtSpace2
|
||||
] = [
|
||||
NoKibanaPrivilegesAtSpace1,
|
||||
SuperuserAtSpace1,
|
||||
GlobalReadAtSpace1,
|
||||
Space1AllAtSpace1,
|
||||
Space1AllAtSpace2,
|
||||
];
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function emailTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
const ES_TEST_INDEX_NAME = 'functional-test-actions-index';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function indexTest({ getService }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function serverLogTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
|
@ -6,10 +6,11 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import { SLACK_ACTION_SIMULATOR_URI } from '../../../fixtures/plugins/actions';
|
||||
import { SLACK_ACTION_SIMULATOR_URI } from '../../../../common/fixtures/plugins/actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function slackTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('create', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle create action request appropriately', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
id: response.body.id,
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
expect(typeof response.body.id).to.be('string');
|
||||
objectRemover.add(space.id, response.body.id, 'action');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should handle create action request appropriately when action type isn't registered`, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.unregistered-action-type',
|
||||
config: {},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Action type "test.unregistered-action-type" is not registered.',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle create action request appropriately when payload is empty and invalid', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'child "description" fails because ["description" is required]. child "actionTypeId" fails because ["actionTypeId" is required]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['description', 'actionTypeId'],
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should handle create action request appropriately when config isn't valid`, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
description: 'my description',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: 'my unencrypted text',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [encrypted]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function deleteActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('delete', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle delete action request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.delete(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(204);
|
||||
expect(response.body).to.eql('');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should handle delete request appropriately when action doesn't exist`, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.delete(`${getUrlPrefix(space.id)}/api/action/2`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,456 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const es = getService('es');
|
||||
|
||||
const esTestIndexName = '.kibaka-alerting-test-data';
|
||||
const authorizationIndex = '.kibana-test-authorization';
|
||||
|
||||
describe('execute', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
before(async () => {
|
||||
await es.indices.delete({ index: esTestIndexName, ignore: [404] });
|
||||
await es.indices.create({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
source: {
|
||||
type: 'keyword',
|
||||
},
|
||||
reference: {
|
||||
type: 'keyword',
|
||||
},
|
||||
params: {
|
||||
enabled: false,
|
||||
type: 'object',
|
||||
},
|
||||
config: {
|
||||
enabled: false,
|
||||
type: 'object',
|
||||
},
|
||||
state: {
|
||||
enabled: false,
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await es.indices.create({ index: authorizationIndex });
|
||||
});
|
||||
after(async () => {
|
||||
await es.indices.delete({ index: esTestIndexName });
|
||||
await es.indices.delete({ index: authorizationIndex });
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
async function getTestIndexDoc(source: string, reference: string) {
|
||||
const searchResult = await es.search({
|
||||
index: esTestIndexName,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
source,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
reference,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(searchResult.hits.total.value).to.eql(1);
|
||||
return searchResult.hits.hits[0];
|
||||
}
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle execute request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
const reference = `actions-execute-1:${user.username}`;
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
reference,
|
||||
index: esTestIndexName,
|
||||
message: 'Testing 123',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.be.an('object');
|
||||
const indexedRecord = await getTestIndexDoc('action:test.index-record', reference);
|
||||
expect(indexedRecord._source).to.eql({
|
||||
params: {
|
||||
reference,
|
||||
index: esTestIndexName,
|
||||
message: 'Testing 123',
|
||||
},
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
reference,
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle execute request appropriately after action is updated', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const reference = `actions-execute-2:${user.username}`;
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
reference,
|
||||
index: esTestIndexName,
|
||||
message: 'Testing 123',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.be.an('object');
|
||||
const indexedRecord = await getTestIndexDoc('action:test.index-record', reference);
|
||||
expect(indexedRecord._source).to.eql({
|
||||
params: {
|
||||
reference,
|
||||
index: esTestIndexName,
|
||||
message: 'Testing 123',
|
||||
},
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
reference,
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should handle execute request appropriately when action doesn't exist`, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action/1/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { foo: true },
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [action/1] not found',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle execute request appropriately when payload is empty and invalid', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action/1/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "params" fails because ["params" is required]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['params'],
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle execute request appropriately after changing config properties', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'test email action',
|
||||
actionTypeId: '.email',
|
||||
config: {
|
||||
from: 'email-from@example.com',
|
||||
host: 'host-is-ignored-here.example.com',
|
||||
port: 666,
|
||||
},
|
||||
secrets: {
|
||||
user: 'email-user',
|
||||
password: 'email-password',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'a test email action 2',
|
||||
config: {
|
||||
from: 'email-from@example.com',
|
||||
service: '__json',
|
||||
},
|
||||
secrets: {
|
||||
user: 'email-user',
|
||||
password: 'email-password',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
to: ['X'],
|
||||
subject: 'email-subject',
|
||||
message: 'email-message',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle execute request appropriately and have proper callCluster and savedObjectsClient authorization', async () => {
|
||||
let indexedRecord: any;
|
||||
const reference = `actions-execute-3:${user.username}`;
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.authorization',
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}/_execute`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
callClusterAuthorizationIndex: authorizationIndex,
|
||||
savedObjectsClientType: 'dashboard',
|
||||
savedObjectsClientId: '1',
|
||||
index: esTestIndexName,
|
||||
reference,
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
indexedRecord = await getTestIndexDoc('action:test.authorization', reference);
|
||||
expect(indexedRecord._source.state).to.eql({
|
||||
callClusterSuccess: false,
|
||||
savedObjectsClientSuccess: false,
|
||||
callClusterError: {
|
||||
...indexedRecord._source.state.callClusterError,
|
||||
msg: `[security_exception] action [indices:data/write/bulk[s]] is unauthorized for user [${user.username}]`,
|
||||
statusCode: 403,
|
||||
},
|
||||
savedObjectsClientError: {
|
||||
...indexedRecord._source.state.savedObjectsClientError,
|
||||
output: {
|
||||
...indexedRecord._source.state.savedObjectsClientError.output,
|
||||
statusCode: 403,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
indexedRecord = await getTestIndexDoc('action:test.authorization', reference);
|
||||
expect(indexedRecord._source.state).to.eql({
|
||||
callClusterSuccess: true,
|
||||
savedObjectsClientSuccess: false,
|
||||
savedObjectsClientError: {
|
||||
...indexedRecord._source.state.savedObjectsClientError,
|
||||
output: {
|
||||
...indexedRecord._source.state.savedObjectsClientError.output,
|
||||
statusCode: 404,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function findActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('find', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle find action request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
space.id
|
||||
)}/api/action/_find?search=test.index-record&search_fields=actionTypeId`
|
||||
)
|
||||
.auth(user.username, user.password);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
data: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function getActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('get', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle get action request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}`)
|
||||
.auth(user.username, user.password);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
id: createdAction.id,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function actionsTests({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Actions', () => {
|
||||
loadTestFile(require.resolve('./create'));
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix } from '../../../common/lib/space_test_utils';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function listActionTypesTests({ getService }: FtrProviderContext) {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('list_action_types', () => {
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should return 200 with list of action types containing defaults', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/api/action/types`)
|
||||
.auth(user.username, user.password);
|
||||
|
||||
function createActionTypeMatcher(id: string, name: string) {
|
||||
return (actionType: { id: string; name: string }) => {
|
||||
return actionType.id === id && actionType.name === name;
|
||||
};
|
||||
}
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
// Check for values explicitly in order to avoid this test failing each time plugins register
|
||||
// a new action type
|
||||
expect(
|
||||
response.body.some(
|
||||
createActionTypeMatcher('test.index-record', 'Test: Index Record')
|
||||
)
|
||||
).to.be(true);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { getUrlPrefix, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function updateActionTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('update', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
describe(scenario.id, () => {
|
||||
it('should handle update action request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
id: createdAction.id,
|
||||
actionTypeId: 'test.index-record',
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle update action request appropriately when passing a null config', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/1`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: null,
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "config" fails because ["config" must be an object]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['config'],
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should handle update action request appropriately when action doesn't exist`, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/1`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [action/1] not found',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle update action request appropriately when payload is empty and invalid', async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/1`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "description" fails because ["description" is required]',
|
||||
validation: { source: 'payload', keys: ['description'] },
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle update action request appropriately when secrets are not valid', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
description: 'My action',
|
||||
actionTypeId: 'test.index-record',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAction.id, 'action');
|
||||
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/action/${createdAction.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
description: 'My action updated',
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 42,
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [encrypted]: expected value of type [string] but got [number]',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
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