Implemented actions server API for supporting preconfigured connectors (#62382)

* Implemented actions server API for supporting preconfigured connectors defined in kibana.yaml

* Fixed type check

* Fixed due to comments and extended functional tests

* Fixed tests and renamed connectors

* fixed jest tests

* Fixed type checks

* Fixed failing alert save

* Fixed alert client tests

* fixed type checks

* Fixed language check error

* Fixed jest tests

* Added missing comments and docs

* fixed due to comments

* Fixed json config for preconfigured

* fixed type check, reverted config

* config experiment with json stringify

* revert experiment

* Removed the spaces from connector names in config
This commit is contained in:
Yuliia Naumenko 2020-04-08 09:54:42 -07:00 committed by GitHub
parent 18c3f75bfb
commit 730dcbf638
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1051 additions and 702 deletions

View file

@ -25,6 +25,7 @@ exports[`Step1 editing should allow for editing 1`] = `
"actionTypeId": "1abc",
"config": Object {},
"id": "1",
"isPreconfigured": false,
"name": "Testing",
}
}

View file

@ -61,7 +61,7 @@ export const AlertsConfiguration: React.FC<AlertsConfigurationProps> = (
async function fetchEmailActions() {
const kibanaActions = await kfetch({
method: 'GET',
pathname: `/api/action/_find`,
pathname: `/api/action/_getAll`,
});
const actions = kibanaActions.data.filter(

View file

@ -27,6 +27,7 @@ describe('Step1', () => {
actionTypeId: '1abc',
name: 'Testing',
config: {},
isPreconfigured: false,
},
];
const selectedEmailActionId = emailActions[0].id;
@ -83,6 +84,7 @@ describe('Step1', () => {
actionTypeId: '.email',
name: '',
config: {},
isPreconfigured: false,
},
],
selectedEmailActionId: NEW_ACTION_ID,

View file

@ -6,7 +6,7 @@
import { isEmpty } from 'lodash/fp';
import {
CasesConnectorsFindResult,
Connector,
CasesConfigurePatch,
CasesConfigureResponse,
CasesConfigureRequest,
@ -18,7 +18,7 @@ import { ApiProps } from '../types';
import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils';
import { CaseConfigure } from './types';
export const fetchConnectors = async ({ signal }: ApiProps): Promise<CasesConnectorsFindResult> => {
export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => {
const response = await KibanaServices.get().http.fetch(
`${CASES_CONFIGURE_URL}/connectors/_find`,
{

View file

@ -31,7 +31,7 @@ export const useConnectors = (): ReturnConnectors => {
const res = await fetchConnectors({ signal: abortCtrl.signal });
if (!didCancel) {
setLoading(false);
setConnectors(res.data);
setConnectors(res);
}
} catch (error) {
if (!didCancel) {

View file

@ -21,6 +21,7 @@ export const connectors: Connector[] = [
id: '123',
actionTypeId: '.servicenow',
name: 'My Connector',
isPreconfigured: false,
config: {
apiUrl: 'https://instance1.service-now.com',
casesConfiguration: {
@ -48,6 +49,7 @@ export const connectors: Connector[] = [
id: '456',
actionTypeId: '.servicenow',
name: 'My Connector 2',
isPreconfigured: false,
config: {
apiUrl: 'https://instance2.service-now.com',
casesConfiguration: {

View file

@ -383,6 +383,7 @@ export const createActionResult = (): ActionResult => ({
actionTypeId: 'action-id-1',
name: '',
config: {},
isPreconfigured: false,
});
export const nonRuleAlert = () => ({
@ -518,6 +519,7 @@ export const updateActionResult = (): ActionResult => ({
actionTypeId: 'action-id-1',
name: '',
config: {},
isPreconfigured: false,
});
export const getMockPrivilegesResult = () => ({

View file

@ -13,5 +13,5 @@ set -e
# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
-X GET ${KIBANA_URL}${SPACE_URL}/api/action/_find \
-X GET ${KIBANA_URL}${SPACE_URL}/api/action/_getAll \
| jq .

View file

@ -28,7 +28,7 @@ Table of Contents
- [RESTful API](#restful-api)
- [`POST /api/action`: Create action](#post-apiaction-create-action)
- [`DELETE /api/action/{id}`: Delete action](#delete-apiactionid-delete-action)
- [`GET /api/action/_find`: Find actions](#get-apiactionfind-find-actions)
- [`GET /api/action/_getAll`: Get all actions](#get-apiaction-get-all-actions)
- [`GET /api/action/{id}`: Get action](#get-apiactionid-get-action)
- [`GET /api/action/types`: List action types](#get-apiactiontypes-list-action-types)
- [`PUT /api/action/{id}`: Update action](#put-apiactionid-update-action)
@ -92,6 +92,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba
| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean |
| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array<String> |
| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array<String> |
| _xpack.actions._**preconfigured** | A list of preconfigured actions. Default: `[]` | Array<Object> |
#### Whitelisting Built-in Action Types
@ -174,11 +175,13 @@ Params:
| -------- | --------------------------------------------- | ------ |
| id | The id of the action you're trying to delete. | string |
### `GET /api/action/_find`: Find actions
### `GET /api/action/_getAll`: Get all actions
Params:
No parameters.
See the [saved objects API documentation for find](https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html). All the properties are the same except that you cannot pass in `type`.
Return all actions from saved objects merged with predefined list.
Use the [saved objects API for find](https://www.elastic.co/guide/en/kibana/master/saved-objects-api-find.html) with the proprties: `type: 'action'` and `perPage: 10000`.
List of predefined actions should be set up in Kibana.yaml.
### `GET /api/action/{id}`: Get action

View file

@ -20,4 +20,5 @@ export interface ActionResult {
actionTypeId: string;
name: string;
config: Record<string, any>;
isPreconfigured: boolean;
}

View file

@ -12,9 +12,9 @@ const createActionsClientMock = () => {
const mocked: jest.Mocked<ActionsClientContract> = {
create: jest.fn(),
get: jest.fn(),
find: jest.fn(),
delete: jest.fn(),
update: jest.fn(),
getAll: jest.fn(),
};
return mocked;
};

View file

@ -51,6 +51,7 @@ beforeEach(() => {
savedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
preconfiguredActions: [],
});
});
@ -83,6 +84,7 @@ describe('create()', () => {
});
expect(result).toEqual({
id: '1',
isPreconfigured: false,
name: 'my name',
actionTypeId: 'my-action-type',
config: {},
@ -178,6 +180,7 @@ describe('create()', () => {
});
expect(result).toEqual({
id: '1',
isPreconfigured: false,
name: 'my name',
actionTypeId: 'my-action-type',
config: {
@ -226,6 +229,7 @@ describe('create()', () => {
savedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
preconfiguredActions: [],
});
const savedObjectCreateResult = {
@ -305,6 +309,7 @@ describe('get()', () => {
const result = await actionsClient.get({ id: '1' });
expect(result).toEqual({
id: '1',
isPreconfigured: false,
});
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
@ -314,9 +319,44 @@ describe('get()', () => {
]
`);
});
test('return predefined action with id', async () => {
actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
preconfiguredActions: [
{
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {
test: 'test1',
},
isPreconfigured: true,
name: 'test',
config: {
foo: 'bar',
},
},
],
});
const result = await actionsClient.get({ id: 'testPreconfigured' });
expect(result).toEqual({
id: 'testPreconfigured',
actionTypeId: '.slack',
isPreconfigured: true,
name: 'test',
config: {
foo: 'bar',
},
});
expect(savedObjectsClient.get).not.toHaveBeenCalled();
});
});
describe('find()', () => {
describe('getAll()', () => {
test('calls savedObjectsClient with parameters', async () => {
const expectedResult = {
total: 1,
@ -327,6 +367,7 @@ describe('find()', () => {
id: '1',
type: 'type',
attributes: {
name: 'test',
config: {
foo: 'bar',
},
@ -339,31 +380,50 @@ describe('find()', () => {
scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({
aggregations: {
'1': { doc_count: 6 },
testPreconfigured: { doc_count: 2 },
},
});
const result = await actionsClient.find({});
expect(result).toEqual({
total: 1,
perPage: 10,
page: 1,
data: [
actionsClient = new ActionsClient({
actionTypeRegistry,
savedObjectsClient,
scopedClusterClient,
defaultKibanaIndex,
preconfiguredActions: [
{
id: '1',
id: 'testPreconfigured',
actionTypeId: '.slack',
secrets: {},
isPreconfigured: true,
name: 'test',
config: {
foo: 'bar',
},
referencedByCount: 6,
},
],
});
expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"type": "action",
const result = await actionsClient.getAll();
expect(result).toEqual([
{
id: '1',
isPreconfigured: false,
name: 'test',
config: {
foo: 'bar',
},
]
`);
referencedByCount: 6,
},
{
id: 'testPreconfigured',
actionTypeId: '.slack',
isPreconfigured: true,
name: 'test',
config: {
foo: 'bar',
},
referencedByCount: 2,
},
]);
});
});
@ -420,6 +480,7 @@ describe('update()', () => {
});
expect(result).toEqual({
id: 'my-action',
isPreconfigured: false,
actionTypeId: 'my-action-type',
name: 'my name',
config: {},
@ -524,6 +585,7 @@ describe('update()', () => {
});
expect(result).toEqual({
id: 'my-action',
isPreconfigured: false,
actionTypeId: 'my-action-type',
name: 'my name',
config: {

View file

@ -11,9 +11,16 @@ import {
SavedObject,
} from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { ActionTypeRegistry } from './action_type_registry';
import { validateConfig, validateSecrets } from './lib';
import { ActionResult, FindActionResult, RawAction } from './types';
import { ActionResult, FindActionResult, RawAction, PreConfiguredAction } from './types';
import { PreconfiguredActionDisabledModificationError } from './lib/errors/preconfigured_action_disabled_modification';
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 10000.
// We'll set this max setting assuming it's never reached.
export const MAX_ACTIONS_RETURNED = 10000;
interface ActionUpdate extends SavedObjectAttributes {
name: string;
@ -29,35 +36,12 @@ interface CreateOptions {
action: Action;
}
interface FindOptions {
options?: {
perPage?: number;
page?: number;
search?: string;
defaultSearchOperator?: 'AND' | 'OR';
searchFields?: string[];
sortField?: string;
hasReference?: {
type: string;
id: string;
};
fields?: string[];
filter?: string;
};
}
interface FindResult {
page: number;
perPage: number;
total: number;
data: FindActionResult[];
}
interface ConstructorOptions {
defaultKibanaIndex: string;
scopedClusterClient: IScopedClusterClient;
actionTypeRegistry: ActionTypeRegistry;
savedObjectsClient: SavedObjectsClientContract;
preconfiguredActions: PreConfiguredAction[];
}
interface UpdateOptions {
@ -70,17 +54,20 @@ export class ActionsClient {
private readonly scopedClusterClient: IScopedClusterClient;
private readonly savedObjectsClient: SavedObjectsClientContract;
private readonly actionTypeRegistry: ActionTypeRegistry;
private readonly preconfiguredActions: PreConfiguredAction[];
constructor({
actionTypeRegistry,
defaultKibanaIndex,
scopedClusterClient,
savedObjectsClient,
preconfiguredActions,
}: ConstructorOptions) {
this.actionTypeRegistry = actionTypeRegistry;
this.savedObjectsClient = savedObjectsClient;
this.scopedClusterClient = scopedClusterClient;
this.defaultKibanaIndex = defaultKibanaIndex;
this.preconfiguredActions = preconfiguredActions;
}
/**
@ -106,6 +93,7 @@ export class ActionsClient {
actionTypeId: result.attributes.actionTypeId,
name: result.attributes.name,
config: result.attributes.config,
isPreconfigured: false,
};
}
@ -113,6 +101,20 @@ export class ActionsClient {
* Update action
*/
public async update({ id, action }: UpdateOptions): Promise<ActionResult> {
if (
this.preconfiguredActions.find(preconfiguredAction => preconfiguredAction.id === id) !==
undefined
) {
throw new PreconfiguredActionDisabledModificationError(
i18n.translate('xpack.actions.serverSideErrors.predefinedActionUpdateDisabled', {
defaultMessage: 'Preconfigured action {id} is not allowed to update.',
values: {
id,
},
}),
'update'
);
}
const existingObject = await this.savedObjectsClient.get<RawAction>('action', id);
const { actionTypeId } = existingObject.attributes;
const { name, config, secrets } = action;
@ -134,6 +136,7 @@ export class ActionsClient {
actionTypeId: result.attributes.actionTypeId as string,
name: result.attributes.name as string,
config: result.attributes.config as Record<string, any>,
isPreconfigured: false,
};
}
@ -141,6 +144,18 @@ export class ActionsClient {
* Get an action
*/
public async get({ id }: { id: string }): Promise<ActionResult> {
const preconfiguredActionsList = this.preconfiguredActions.find(
preconfiguredAction => preconfiguredAction.id === id
);
if (preconfiguredActionsList !== undefined) {
return {
id,
actionTypeId: preconfiguredActionsList.actionTypeId,
name: preconfiguredActionsList.name,
config: preconfiguredActionsList.config,
isPreconfigured: true,
};
}
const result = await this.savedObjectsClient.get<RawAction>('action', id);
return {
@ -148,36 +163,56 @@ export class ActionsClient {
actionTypeId: result.attributes.actionTypeId,
name: result.attributes.name,
config: result.attributes.config,
isPreconfigured: false,
};
}
/**
* Find actions
* Get all actions with preconfigured list
*/
public async find({ options = {} }: FindOptions): Promise<FindResult> {
const findResult = await this.savedObjectsClient.find<RawAction>({
...options,
type: 'action',
});
public async getAll(): Promise<FindActionResult[]> {
const savedObjectsActions = (
await this.savedObjectsClient.find<RawAction>({
perPage: MAX_ACTIONS_RETURNED,
type: 'action',
})
).saved_objects.map(actionFromSavedObject);
const data = await injectExtraFindData(
const mergedResult = [
...savedObjectsActions,
...this.preconfiguredActions.map(preconfiguredAction => ({
id: preconfiguredAction.id,
actionTypeId: preconfiguredAction.actionTypeId,
name: preconfiguredAction.name,
config: preconfiguredAction.config,
isPreconfigured: true,
})),
].sort((a, b) => a.name.localeCompare(b.name));
return await injectExtraFindData(
this.defaultKibanaIndex,
this.scopedClusterClient,
findResult.saved_objects.map(actionFromSavedObject)
mergedResult
);
return {
page: findResult.page,
perPage: findResult.per_page,
total: findResult.total,
data,
};
}
/**
* Delete action
*/
public async delete({ id }: { id: string }) {
if (
this.preconfiguredActions.find(preconfiguredAction => preconfiguredAction.id === id) !==
undefined
) {
throw new PreconfiguredActionDisabledModificationError(
i18n.translate('xpack.actions.serverSideErrors.predefinedActionDeleteDisabled', {
defaultMessage: 'Preconfigured action {id} is not allowed to delete.',
values: {
id,
},
}),
'delete'
);
}
return await this.savedObjectsClient.delete('action', id);
}
}
@ -186,6 +221,7 @@ function actionFromSavedObject(savedObject: SavedObject<RawAction>): ActionResul
return {
id: savedObject.id,
...savedObject.attributes,
isPreconfigured: false,
};
}

View file

@ -14,6 +14,44 @@ describe('config validation', () => {
"enabledActionTypes": Array [
"*",
],
"preconfigured": Array [],
"whitelistedHosts": Array [
"*",
],
}
`);
});
test('action with preconfigured actions', () => {
const config: Record<string, any> = {
preconfigured: [
{
id: 'my-slack1',
actionTypeId: '.slack',
name: 'Slack #xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
},
],
};
expect(configSchema.validate(config)).toMatchInlineSnapshot(`
Object {
"enabled": true,
"enabledActionTypes": Array [
"*",
],
"preconfigured": Array [
Object {
"actionTypeId": ".slack",
"config": Object {
"webhookUrl": "https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz",
},
"id": "my-slack1",
"name": "Slack #xyz",
"secrets": Object {},
},
],
"whitelistedHosts": Array [
"*",
],

View file

@ -21,6 +21,18 @@ export const configSchema = schema.object({
defaultValue: [WhitelistedHosts.Any],
}
),
preconfigured: schema.arrayOf(
schema.object({
id: schema.string({ minLength: 1 }),
name: schema.string(),
actionTypeId: schema.string({ minLength: 1 }),
config: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
}),
{
defaultValue: [],
}
),
});
export type ActionsConfig = TypeOf<typeof configSchema>;

View file

@ -11,7 +11,13 @@ import { ActionsClient as ActionsClientClass } from './actions_client';
export type ActionsClient = PublicMethodsOf<ActionsClientClass>;
export { ActionsPlugin, ActionResult, ActionTypeExecutorOptions, ActionType } from './types';
export {
ActionsPlugin,
ActionResult,
ActionTypeExecutorOptions,
ActionType,
PreConfiguredAction,
} from './types';
export { PluginSetupContract, PluginStartContract } from './plugin';
export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext);

View file

@ -0,0 +1,24 @@
/*
* 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 { KibanaResponseFactory } from '../../../../../../src/core/server';
import { ErrorThatHandlesItsOwnResponse } from './types';
export type PreconfiguredActionDisabledFrom = 'update' | 'delete';
export class PreconfiguredActionDisabledModificationError extends Error
implements ErrorThatHandlesItsOwnResponse {
public readonly disabledFrom: PreconfiguredActionDisabledFrom;
constructor(message: string, disabledFrom: PreconfiguredActionDisabledFrom) {
super(message);
this.disabledFrom = disabledFrom;
}
public sendResponse(res: KibanaResponseFactory) {
return res.badRequest({ body: { message: this.message } });
}
}

View file

@ -21,6 +21,7 @@ const createStartMock = () => {
execute: jest.fn(),
isActionTypeEnabled: jest.fn(),
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
preconfiguredActions: [],
};
return mock;
};

View file

@ -31,7 +31,34 @@ describe('Actions Plugin', () => {
let pluginsSetup: jest.Mocked<ActionsPluginsSetup>;
beforeEach(() => {
context = coreMock.createPluginInitializerContext();
context = coreMock.createPluginInitializerContext({
preconfigured: [
{
id: 'my-slack1',
actionTypeId: '.slack',
name: 'Slack #xyz',
description: 'Send a message to the #xyz channel',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
},
{
id: 'custom-system-abc-connector',
actionTypeId: 'system-abc-action-type',
description: 'Send a notification to system ABC',
name: 'System ABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
secrets: {
xyzSecret1: 'credential1',
xyzSecret2: 'credential2',
},
},
],
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
@ -160,7 +187,9 @@ describe('Actions Plugin', () => {
let pluginsStart: jest.Mocked<ActionsPluginsStart>;
beforeEach(() => {
const context = coreMock.createPluginInitializerContext();
const context = coreMock.createPluginInitializerContext({
preconfigured: [],
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
coreStart = coreMock.createStart();

View file

@ -30,7 +30,7 @@ import { LICENSE_TYPE } from '../../licensing/common/types';
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
import { ActionsConfig } from './config';
import { Services, ActionType } from './types';
import { Services, ActionType, PreConfiguredAction } from './types';
import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib';
import { ActionsClient } from './actions_client';
import { ActionTypeRegistry } from './action_type_registry';
@ -44,7 +44,7 @@ import { getActionsConfigurationUtilities } from './actions_config';
import {
createActionRoute,
deleteActionRoute,
findActionRoute,
getAllActionRoute,
getActionRoute,
updateActionRoute,
listActionTypesRoute,
@ -67,6 +67,7 @@ export interface PluginStartContract {
isActionTypeEnabled(id: string): boolean;
execute(options: ExecuteOptions): Promise<void>;
getActionsClientWithRequest(request: KibanaRequest): Promise<PublicMethodsOf<ActionsClient>>;
preconfiguredActions: PreConfiguredAction[];
}
export interface ActionsPluginsSetup {
@ -97,6 +98,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
private eventLogger?: IEventLogger;
private isESOUsingEphemeralEncryptionKey?: boolean;
private readonly telemetryLogger: Logger;
private readonly preconfiguredActions: PreConfiguredAction[];
constructor(initContext: PluginInitializerContext) {
this.config = initContext.config
@ -113,6 +115,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
this.logger = initContext.logger.get('actions');
this.telemetryLogger = initContext.logger.get('telemetry');
this.preconfiguredActions = [];
}
public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise<PluginSetupContract> {
@ -151,8 +154,14 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
// get executions count
const taskRunnerFactory = new TaskRunnerFactory(actionExecutor);
const actionsConfigUtils = getActionsConfigurationUtilities(
(await this.config) as ActionsConfig
const actionsConfig = (await this.config) as ActionsConfig;
const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig);
this.preconfiguredActions.push(
...actionsConfig.preconfigured.map(
preconfiguredAction =>
({ ...preconfiguredAction, isPreconfigured: true } as PreConfiguredAction)
)
);
const actionTypeRegistry = new ActionTypeRegistry({
taskRunnerFactory,
@ -197,7 +206,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
createActionRoute(router, this.licenseState);
deleteActionRoute(router, this.licenseState);
getActionRoute(router, this.licenseState);
findActionRoute(router, this.licenseState);
getAllActionRoute(router, this.licenseState);
updateActionRoute(router, this.licenseState);
listActionTypesRoute(router, this.licenseState);
executeActionRoute(router, this.licenseState, actionExecutor);
@ -226,6 +235,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
kibanaIndex,
adminClient,
isESOUsingEphemeralEncryptionKey,
preconfiguredActions,
} = this;
actionExecutor!.initialize({
@ -271,8 +281,10 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
actionTypeRegistry: actionTypeRegistry!,
defaultKibanaIndex: await kibanaIndex,
scopedClusterClient: adminClient!.asScoped(request),
preconfiguredActions,
});
},
preconfiguredActions,
};
}
@ -289,7 +301,12 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
private createRouteHandlerContext = (
defaultKibanaIndex: string
): IContextProvider<RequestHandler<any, any, any>, 'actions'> => {
const { actionTypeRegistry, adminClient, isESOUsingEphemeralEncryptionKey } = this;
const {
actionTypeRegistry,
adminClient,
isESOUsingEphemeralEncryptionKey,
preconfiguredActions,
} = this;
return async function actionsRouteHandlerContext(context, request) {
return {
getActionsClient: () => {
@ -303,6 +320,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
actionTypeRegistry: actionTypeRegistry!,
defaultKibanaIndex,
scopedClusterClient: adminClient!.asScoped(request),
preconfiguredActions,
});
},
listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!),

View file

@ -17,7 +17,7 @@ import {
IKibanaResponse,
KibanaResponseFactory,
} from 'kibana/server';
import { ILicenseState, verifyApiAccess } from '../lib';
import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib';
import { BASE_ACTION_API_PATH } from '../../common';
const paramSchema = schema.object({
@ -46,8 +46,15 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState)
}
const actionsClient = context.actions.getActionsClient();
const { id } = req.params;
await actionsClient.delete({ id });
return res.noContent();
try {
await actionsClient.delete({ id });
return res.noContent();
} catch (e) {
if (isErrorThatHandlesItsOwnResponse(e)) {
return e.sendResponse(res);
}
throw e;
}
})
);
};

View file

@ -1,152 +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 { findActionRoute } from './find';
import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
jest.mock('../lib/verify_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
});
describe('findActionRoute', () => {
it('finds actions with proper parameters', async () => {
const licenseState = licenseStateMock.create();
const router: RouterMock = mockRouter.create();
findActionRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/action/_find"`);
expect(config.options).toMatchInlineSnapshot(`
Object {
"tags": Array [
"access:actions-read",
],
}
`);
const findResult = {
page: 1,
perPage: 1,
total: 0,
data: [],
};
const actionsClient = {
find: jest.fn().mockResolvedValueOnce(findResult),
};
const [context, req, res] = mockHandlerArguments(
{ actionsClient },
{
query: {
per_page: 1,
page: 1,
default_search_operator: 'OR',
},
},
['ok']
);
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"data": Array [],
"page": 1,
"perPage": 1,
"total": 0,
},
}
`);
expect(actionsClient.find).toHaveBeenCalledTimes(1);
expect(actionsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"options": Object {
"defaultSearchOperator": "OR",
"fields": undefined,
"filter": undefined,
"page": 1,
"perPage": 1,
"search": undefined,
"sortField": undefined,
"sortOrder": undefined,
},
},
]
`);
expect(res.ok).toHaveBeenCalledWith({
body: findResult,
});
});
it('ensures the license allows finding actions', async () => {
const licenseState = licenseStateMock.create();
const router: RouterMock = mockRouter.create();
findActionRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];
const actionsClient = {
find: jest.fn().mockResolvedValueOnce({
page: 1,
perPage: 1,
total: 0,
data: [],
}),
};
const [context, req, res] = mockHandlerArguments(actionsClient, {
query: {
per_page: 1,
page: 1,
default_search_operator: 'OR',
},
});
await handler(context, req, res);
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});
it('ensures the license check prevents finding actions', async () => {
const licenseState = licenseStateMock.create();
const router: RouterMock = mockRouter.create();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
});
findActionRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];
const [context, req, res] = mockHandlerArguments(
{},
{
query: {
per_page: 1,
page: 1,
default_search_operator: 'OR',
},
},
['ok']
);
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});
});

View file

@ -1,95 +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 { schema, TypeOf } from '@kbn/config-schema';
import {
IRouter,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
} from 'kibana/server';
import { FindOptions } from '../../../alerting/server';
import { ILicenseState, verifyApiAccess } from '../lib';
import { BASE_ACTION_API_PATH } from '../../common';
// config definition
const querySchema = schema.object({
per_page: schema.number({ defaultValue: 20, min: 0 }),
page: schema.number({ defaultValue: 1, min: 1 }),
search: schema.maybe(schema.string()),
default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], {
defaultValue: 'OR',
}),
search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])),
sort_field: schema.maybe(schema.string()),
sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])),
has_reference: schema.maybe(
// use nullable as maybe is currently broken
// in config-schema
schema.nullable(
schema.object({
type: schema.string(),
id: schema.string(),
})
)
),
fields: schema.maybe(schema.arrayOf(schema.string())),
filter: schema.maybe(schema.string()),
});
export const findActionRoute = (router: IRouter, licenseState: ILicenseState) => {
router.get(
{
path: `${BASE_ACTION_API_PATH}/_find`,
validate: {
query: querySchema,
},
options: {
tags: ['access:actions-read'],
},
},
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<any, TypeOf<typeof querySchema>, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
verifyApiAccess(licenseState);
if (!context.actions) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
}
const actionsClient = context.actions.getActionsClient();
const query = req.query;
const options: FindOptions['options'] = {
perPage: query.per_page,
page: query.page,
search: query.search,
defaultSearchOperator: query.default_search_operator,
sortField: query.sort_field,
fields: query.fields,
filter: query.filter,
sortOrder: query.sort_order,
};
if (query.search_fields) {
options.searchFields = Array.isArray(query.search_fields)
? query.search_fields
: [query.search_fields];
}
if (query.has_reference) {
options.hasReference = query.has_reference;
}
const findResult = await actionsClient.find({
options,
});
return res.ok({
body: findResult,
});
})
);
};

View file

@ -0,0 +1,117 @@
/*
* 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 { getAllActionRoute } from './get_all';
import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib';
import { mockHandlerArguments } from './_mock_handler_arguments';
jest.mock('../lib/verify_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
});
describe('getAllActionRoute', () => {
it('get all actions with proper parameters', async () => {
const licenseState = licenseStateMock.create();
const router: RouterMock = mockRouter.create();
getAllActionRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`);
expect(config.options).toMatchInlineSnapshot(`
Object {
"tags": Array [
"access:actions-read",
],
}
`);
const actionsClient = {
getAll: jest.fn().mockResolvedValueOnce([]),
};
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Array [],
}
`);
expect(actionsClient.getAll).toHaveBeenCalledTimes(1);
expect(res.ok).toHaveBeenCalledWith({
body: [],
});
});
it('ensures the license allows getting all actions', async () => {
const licenseState = licenseStateMock.create();
const router: RouterMock = mockRouter.create();
getAllActionRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`);
expect(config.options).toMatchInlineSnapshot(`
Object {
"tags": Array [
"access:actions-read",
],
}
`);
const actionsClient = {
getAll: jest.fn().mockResolvedValueOnce([]),
};
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
await handler(context, req, res);
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});
it('ensures the license check prevents getting all actions', async () => {
const licenseState = licenseStateMock.create();
const router: RouterMock = mockRouter.create();
(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
});
getAllActionRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/api/action/_getAll"`);
expect(config.options).toMatchInlineSnapshot(`
Object {
"tags": Array [
"access:actions-read",
],
}
`);
const actionsClient = {
getAll: jest.fn().mockResolvedValueOnce([]),
};
const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']);
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
});
});

View file

@ -0,0 +1,42 @@
/*
* 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 {
IRouter,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
} from 'kibana/server';
import { ILicenseState, verifyApiAccess } from '../lib';
import { BASE_ACTION_API_PATH } from '../../common';
export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) => {
router.get(
{
path: `${BASE_ACTION_API_PATH}/_getAll`,
validate: {},
options: {
tags: ['access:actions-read'],
},
},
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<any, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
verifyApiAccess(licenseState);
if (!context.actions) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' });
}
const actionsClient = context.actions.getActionsClient();
const result = await actionsClient.getAll();
return res.ok({
body: result,
});
})
);
};

View file

@ -6,7 +6,7 @@
export { createActionRoute } from './create';
export { deleteActionRoute } from './delete';
export { findActionRoute } from './find';
export { getAllActionRoute } from './get_all';
export { getActionRoute } from './get';
export { updateActionRoute } from './update';
export { listActionTypesRoute } from './list_action_types';

View file

@ -55,6 +55,11 @@ export interface ActionResult {
actionTypeId: string;
name: string;
config: Record<string, any>;
isPreconfigured: boolean;
}
export interface PreConfiguredAction extends ActionResult {
secrets: Record<string, any>;
}
export interface FindActionResult extends ActionResult {

View file

@ -30,6 +30,7 @@ const alertsClientParams = {
invalidateAPIKey: jest.fn(),
logger: loggingServiceMock.create().get(),
encryptedSavedObjectsPlugin: encryptedSavedObjects,
preconfiguredActions: [],
};
beforeEach(() => {

View file

@ -13,6 +13,7 @@ import {
SavedObjectReference,
SavedObject,
} from 'src/core/server';
import { PreConfiguredAction } from '../../actions/server';
import {
Alert,
PartialAlert,
@ -53,6 +54,7 @@ interface ConstructorOptions {
getUserName: () => Promise<string | null>;
createAPIKey: () => Promise<CreateAPIKeyResult>;
invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise<InvalidateAPIKeyResult>;
preconfiguredActions: PreConfiguredAction[];
}
export interface FindOptions {
@ -123,6 +125,7 @@ export class AlertsClient {
private readonly invalidateAPIKey: (
params: InvalidateAPIKeyParams
) => Promise<InvalidateAPIKeyResult>;
private preconfiguredActions: PreConfiguredAction[];
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart;
constructor({
@ -136,6 +139,7 @@ export class AlertsClient {
createAPIKey,
invalidateAPIKey,
encryptedSavedObjectsPlugin,
preconfiguredActions,
}: ConstructorOptions) {
this.logger = logger;
this.getUserName = getUserName;
@ -147,6 +151,7 @@ export class AlertsClient {
this.createAPIKey = createAPIKey;
this.invalidateAPIKey = invalidateAPIKey;
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
this.preconfiguredActions = preconfiguredActions;
}
public async create({ data, options }: CreateOptions): Promise<Alert> {
@ -659,18 +664,37 @@ export class AlertsClient {
private async denormalizeActions(
alertActions: NormalizedAlertAction[]
): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> {
// Fetch action objects in bulk
const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))];
const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' }));
const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts);
const actionMap = new Map<string, any>();
for (const action of bulkGetResult.saved_objects) {
if (action.error) {
throw Boom.badRequest(
`Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}`
);
// map preconfigured actions
for (const alertAction of alertActions) {
const action = this.preconfiguredActions.find(
preconfiguredAction => preconfiguredAction.id === alertAction.id
);
if (action !== undefined) {
actionMap.set(action.id, action);
}
}
// Fetch action objects in bulk
// Excluding preconfigured actions to avoid an not found error, which is already mapped
const actionIds = [
...new Set(
alertActions
.filter(alertAction => !actionMap.has(alertAction.id))
.map(alertAction => alertAction.id)
),
];
if (actionIds.length > 0) {
const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' }));
const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts);
for (const action of bulkGetResult.saved_objects) {
if (action.error) {
throw Boom.badRequest(
`Failed to load action ${action.id} (${action.error.statusCode}): ${action.error.message}`
);
}
actionMap.set(action.id, action);
}
actionMap.set(action.id, action);
}
// Extract references and set actionTypeId
const references: SavedObjectReference[] = [];
@ -681,10 +705,16 @@ export class AlertsClient {
name: actionRef,
type: 'action',
});
const actionMapValue = actionMap.get(id);
// if action is a save object, than actionTypeId should be under attributes property
// if action is a preconfigured, than actionTypeId is the action property
const actionTypeId = actionIds.find(actionId => actionId === id)
? actionMapValue.attributes.actionTypeId
: actionMapValue.actionTypeId;
return {
...alertAction,
actionRef,
actionTypeId: actionMap.get(id).attributes.actionTypeId,
actionTypeId,
};
});
return {

View file

@ -28,6 +28,7 @@ const alertsClientFactoryParams: jest.Mocked<AlertsClientFactoryOpts> = {
getSpaceId: jest.fn(),
spaceIdToNamespace: jest.fn(),
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(),
preconfiguredActions: [],
};
const fakeRequest: Request = {
headers: {},
@ -67,6 +68,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
createAPIKey: expect.any(Function),
invalidateAPIKey: expect.any(Function),
encryptedSavedObjectsPlugin: alertsClientFactoryParams.encryptedSavedObjectsPlugin,
preconfiguredActions: [],
});
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PreConfiguredAction } from '../../actions/server';
import { AlertsClient } from './alerts_client';
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server';
@ -19,6 +20,7 @@ export interface AlertsClientFactoryOpts {
getSpaceId: (request: KibanaRequest) => string | undefined;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart;
preconfiguredActions: PreConfiguredAction[];
}
export class AlertsClientFactory {
@ -30,6 +32,7 @@ export class AlertsClientFactory {
private getSpaceId!: (request: KibanaRequest) => string | undefined;
private spaceIdToNamespace!: SpaceIdToNamespaceFunction;
private encryptedSavedObjectsPlugin!: EncryptedSavedObjectsPluginStart;
private preconfiguredActions!: PreConfiguredAction[];
public initialize(options: AlertsClientFactoryOpts) {
if (this.isInitialized) {
@ -43,6 +46,7 @@ export class AlertsClientFactory {
this.securityPluginSetup = options.securityPluginSetup;
this.spaceIdToNamespace = options.spaceIdToNamespace;
this.encryptedSavedObjectsPlugin = options.encryptedSavedObjectsPlugin;
this.preconfiguredActions = options.preconfiguredActions;
}
public create(
@ -100,6 +104,7 @@ export class AlertsClientFactory {
result: invalidateAPIKeyResult,
};
},
preconfiguredActions: this.preconfiguredActions,
});
}
}

View file

@ -218,6 +218,7 @@ export class AlertingPlugin {
getSpaceId(request: KibanaRequest) {
return spaces?.getSpaceId(request);
},
preconfiguredActions: plugins.actions.preconfiguredActions,
});
taskRunnerFactory.initialize({

View file

@ -61,13 +61,6 @@ export type CasesConnectorConfiguration = rt.TypeOf<typeof CasesConnectorConfigu
export type Connector = ActionResult;
export interface CasesConnectorsFindResult {
page: number;
perPage: number;
total: number;
data: Connector[];
}
// TO DO we will need to add this type rt.literal('close-by-thrid-party')
const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]);

View file

@ -28,12 +28,10 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou
throw Boom.notFound('Action client have not been found');
}
const results = await actionsClient.find({
options: {
filter: `action.attributes.actionTypeId: ${CASE_SERVICE_NOW_ACTION}`,
},
});
return response.ok({ body: { ...results } });
const results = (await actionsClient.getAll()).filter(
action => action.actionTypeId === CASE_SERVICE_NOW_ACTION
);
return response.ok({ body: results });
} catch (error) {
return response.customError(wrapError(error));
}

View file

@ -39,6 +39,7 @@ describe('connector validation', () => {
id: 'test',
actionTypeId: '.email',
name: 'email',
isPreconfigured: false,
config: {
from: 'test@test.com',
port: 2323,
@ -66,6 +67,7 @@ describe('connector validation', () => {
},
id: 'test',
actionTypeId: '.email',
isPreconfigured: false,
name: 'email',
config: {
from: 'test@test.com',
@ -117,6 +119,7 @@ describe('connector validation', () => {
},
id: 'test',
actionTypeId: '.email',
isPreconfigured: false,
name: 'email',
config: {
from: 'test@test.com',
@ -144,6 +147,7 @@ describe('connector validation', () => {
},
id: 'test',
actionTypeId: '.email',
isPreconfigured: false,
name: 'email',
config: {
from: 'test@test.com',

View file

@ -39,6 +39,7 @@ describe('webhook connector validation', () => {
id: 'test',
actionTypeId: '.webhook',
name: 'webhook',
isPreconfigured: false,
config: {
method: 'PUT',
url: 'http:\\test',
@ -106,6 +107,7 @@ describe('WebhookActionConnectorFields renders', () => {
},
id: 'test',
actionTypeId: '.webhook',
isPreconfigured: false,
name: 'webhook',
config: {
method: 'PUT',

View file

@ -43,27 +43,14 @@ describe('loadActionTypes', () => {
});
describe('loadAllActions', () => {
test('should call find actions API', async () => {
const resolvedValue = {
page: 1,
perPage: 10000,
total: 0,
data: [],
};
http.get.mockResolvedValueOnce(resolvedValue);
test('should call getAll actions API', async () => {
http.get.mockResolvedValueOnce([]);
const result = await loadAllActions({ http });
expect(result).toEqual(resolvedValue);
expect(result).toEqual([]);
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/api/action/_find",
Object {
"query": Object {
"per_page": 10000,
"sort_field": "name.keyword",
"sort_order": "asc",
},
},
"/api/action/_getAll",
]
`);
});
@ -73,6 +60,7 @@ describe('createActionConnector', () => {
test('should call create action API', async () => {
const connector: ActionConnectorWithoutId = {
actionTypeId: 'test',
isPreconfigured: false,
name: 'My test',
config: {},
secrets: {},
@ -86,7 +74,7 @@ describe('createActionConnector', () => {
Array [
"/api/action",
Object {
"body": "{\\"actionTypeId\\":\\"test\\",\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}",
"body": "{\\"actionTypeId\\":\\"test\\",\\"isPreconfigured\\":false,\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}",
},
]
`);
@ -98,6 +86,7 @@ describe('updateActionConnector', () => {
const id = '123';
const connector: ActionConnectorWithoutId = {
actionTypeId: 'test',
isPreconfigured: false,
name: 'My test',
config: {},
secrets: {},

View file

@ -8,32 +8,12 @@ import { HttpSetup } from 'kibana/public';
import { BASE_ACTION_API_PATH } from '../constants';
import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types';
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 100 or so.
// We'll set this max setting assuming it's never reached.
const MAX_ACTIONS_RETURNED = 10000;
export async function loadActionTypes({ http }: { http: HttpSetup }): Promise<ActionType[]> {
return await http.get(`${BASE_ACTION_API_PATH}/types`);
}
export async function loadAllActions({
http,
}: {
http: HttpSetup;
}): Promise<{
page: number;
perPage: number;
total: number;
data: ActionConnector[];
}> {
return await http.get(`${BASE_ACTION_API_PATH}/_find`, {
query: {
per_page: MAX_ACTIONS_RETURNED,
sort_field: 'name.keyword',
sort_order: 'asc',
},
});
export async function loadAllActions({ http }: { http: HttpSetup }): Promise<ActionConnector[]> {
return await http.get(`${BASE_ACTION_API_PATH}/_getAll`);
}
export async function createActionConnector({

View file

@ -11,6 +11,10 @@ import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
import { ActionForm } from './action_form';
jest.mock('../../lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
loadActionTypes: jest.fn(),
}));
const actionTypeRegistry = actionTypeRegistryMock.create();
describe('action_form', () => {
let deps: any;
@ -73,6 +77,17 @@ describe('action_form', () => {
let wrapper: ReactWrapper<any>;
async function setup() {
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce([
{
secrets: {},
id: 'test',
actionTypeId: actionType.id,
name: 'Test connector',
config: {},
isPreconfigured: false,
},
]);
const mockes = coreMock.createSetup();
deps = {
toastNotifications: mockes.notifications.toasts,

View file

@ -129,7 +129,7 @@ export const ActionForm = ({
async function loadConnectors() {
try {
const actionsResponse = await loadAllActions({ http });
setConnectors(actionsResponse.data);
setConnectors(actionsResponse);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(

View file

@ -47,6 +47,7 @@ describe('connector_edit_flyout', () => {
actionTypeId: 'test-action-type-id',
actionType: 'test-action-type-name',
name: 'action-connector',
isPreconfigured: false,
referencedByCount: 0,
config: {},
};

View file

@ -15,6 +15,7 @@ describe('connector reducer', () => {
actionTypeId: 'test-action-type-id',
name: 'action-connector',
referencedByCount: 0,
isPreconfigured: false,
config: {},
};
});

View file

@ -29,12 +29,7 @@ describe('actions_connectors_list component empty', () => {
const { loadAllActions, loadActionTypes } = jest.requireMock(
'../../../lib/action_connector_api'
);
loadAllActions.mockResolvedValueOnce({
page: 1,
perPage: 10000,
total: 0,
data: [],
});
loadAllActions.mockResolvedValueOnce([]);
loadActionTypes.mockResolvedValueOnce([
{
id: 'test',
@ -111,27 +106,22 @@ describe('actions_connectors_list component with items', () => {
const { loadAllActions, loadActionTypes } = jest.requireMock(
'../../../lib/action_connector_api'
);
loadAllActions.mockResolvedValueOnce({
page: 1,
perPage: 10000,
total: 2,
data: [
{
id: '1',
actionTypeId: 'test',
description: 'My test',
referencedByCount: 1,
config: {},
},
{
id: '2',
actionTypeId: 'test2',
description: 'My test 2',
referencedByCount: 1,
config: {},
},
],
});
loadAllActions.mockResolvedValueOnce([
{
id: '1',
actionTypeId: 'test',
description: 'My test',
referencedByCount: 1,
config: {},
},
{
id: '2',
actionTypeId: 'test2',
description: 'My test 2',
referencedByCount: 1,
config: {},
},
]);
loadActionTypes.mockResolvedValueOnce([
{
id: 'test',
@ -214,12 +204,7 @@ describe('actions_connectors_list component empty with show only capability', ()
const { loadAllActions, loadActionTypes } = jest.requireMock(
'../../../lib/action_connector_api'
);
loadAllActions.mockResolvedValueOnce({
page: 1,
perPage: 10000,
total: 0,
data: [],
});
loadAllActions.mockResolvedValueOnce([]);
loadActionTypes.mockResolvedValueOnce([
{
id: 'test',
@ -289,27 +274,22 @@ describe('actions_connectors_list with show only capability', () => {
const { loadAllActions, loadActionTypes } = jest.requireMock(
'../../../lib/action_connector_api'
);
loadAllActions.mockResolvedValueOnce({
page: 1,
perPage: 10000,
total: 2,
data: [
{
id: '1',
actionTypeId: 'test',
description: 'My test',
referencedByCount: 1,
config: {},
},
{
id: '2',
actionTypeId: 'test2',
description: 'My test 2',
referencedByCount: 1,
config: {},
},
],
});
loadAllActions.mockResolvedValueOnce([
{
id: '1',
actionTypeId: 'test',
description: 'My test',
referencedByCount: 1,
config: {},
},
{
id: '2',
actionTypeId: 'test2',
description: 'My test 2',
referencedByCount: 1,
config: {},
},
]);
loadActionTypes.mockResolvedValueOnce([
{
id: 'test',
@ -384,27 +364,22 @@ describe('actions_connectors_list component with disabled items', () => {
const { loadAllActions, loadActionTypes } = jest.requireMock(
'../../../lib/action_connector_api'
);
loadAllActions.mockResolvedValueOnce({
page: 1,
perPage: 10000,
total: 2,
data: [
{
id: '1',
actionTypeId: 'test',
description: 'My test',
referencedByCount: 1,
config: {},
},
{
id: '2',
actionTypeId: 'test2',
description: 'My test 2',
referencedByCount: 1,
config: {},
},
],
});
loadAllActions.mockResolvedValueOnce([
{
id: '1',
actionTypeId: 'test',
description: 'My test',
referencedByCount: 1,
config: {},
},
{
id: '2',
actionTypeId: 'test2',
description: 'My test 2',
referencedByCount: 1,
config: {},
},
]);
loadActionTypes.mockResolvedValueOnce([
{
id: 'test',

View file

@ -110,7 +110,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
setIsLoadingActions(true);
try {
const actionsResponse = await loadAllActions({ http });
setActions(actionsResponse.data);
setActions(actionsResponse);
} catch (e) {
toastNotifications.addDanger({
title: i18n.translate(

View file

@ -72,12 +72,7 @@ describe('alerts_list component empty', () => {
},
]);
loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
loadAllActions.mockResolvedValue({
page: 1,
perPage: 10000,
total: 0,
data: [],
});
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
const [
@ -196,12 +191,7 @@ describe('alerts_list component with items', () => {
},
]);
loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
loadAllActions.mockResolvedValue({
page: 1,
perPage: 10000,
total: 0,
data: [],
});
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
const [
{
@ -286,12 +276,7 @@ describe('alerts_list component empty with show only capability', () => {
},
]);
loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
loadAllActions.mockResolvedValue({
page: 1,
perPage: 10000,
total: 0,
data: [],
});
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
const [
{
@ -405,12 +390,7 @@ describe('alerts_list with show only capability', () => {
},
]);
loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]);
loadAllActions.mockResolvedValue({
page: 1,
perPage: 10000,
total: 0,
data: [],
});
loadAllActions.mockResolvedValue([]);
const mockes = coreMock.createSetup();
const [
{

View file

@ -66,6 +66,7 @@ export interface ActionConnector {
name: string;
referencedByCount?: number;
config: Record<string, any>;
isPreconfigured: boolean;
}
export type ActionConnectorWithoutId = Omit<ActionConnector, 'id'>;

View file

@ -77,6 +77,30 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
'--xpack.alerting.enabled=true',
'--xpack.eventLog.logEntries=true',
`--xpack.actions.preconfigured=${JSON.stringify([
{
id: 'my-slack1',
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
},
{
id: 'custom-system-abc-connector',
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
secrets: {
xyzSecret1: 'credential1',
xyzSecret2: 'credential2',
},
},
])}`,
...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,

View file

@ -39,6 +39,7 @@ export default function emailTest({ getService }: FtrProviderContext) {
createdActionId = createdAction.id;
expect(createdAction).to.eql({
id: createdActionId,
isPreconfigured: false,
name: 'An email action',
actionTypeId: '.email',
config: {
@ -58,6 +59,7 @@ export default function emailTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'An email action',
actionTypeId: '.email',
config: {

View file

@ -40,6 +40,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'An index action',
actionTypeId: '.index',
config: {
@ -57,6 +58,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'An index action',
actionTypeId: '.index',
config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null },
@ -79,6 +81,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(createdActionWithIndex).to.eql({
id: createdActionWithIndex.id,
isPreconfigured: false,
name: 'An index action with index config',
actionTypeId: '.index',
config: {
@ -96,6 +99,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(fetchedActionWithIndex).to.eql({
id: fetchedActionWithIndex.id,
isPreconfigured: false,
name: 'An index action with index config',
actionTypeId: '.index',
config: {

View file

@ -50,6 +50,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'A pagerduty action',
actionTypeId: '.pagerduty',
config: {
@ -65,6 +66,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'A pagerduty action',
actionTypeId: '.pagerduty',
config: {

View file

@ -31,6 +31,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) {
serverLogActionId = createdAction.id;
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'A server.log action',
actionTypeId: '.server-log',
config: {},
@ -44,6 +45,7 @@ export default function serverLogTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'A server.log action',
actionTypeId: '.server-log',
config: {},

View file

@ -101,6 +101,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'A servicenow action',
actionTypeId: '.servicenow',
config: {
@ -117,6 +118,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'A servicenow action',
actionTypeId: '.servicenow',
config: {

View file

@ -47,6 +47,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'A slack action',
actionTypeId: '.slack',
config: {},
@ -60,6 +61,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'A slack action',
actionTypeId: '.slack',
config: {},

View file

@ -92,6 +92,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'A generic Webhook action',
actionTypeId: '.webhook',
config: {
@ -108,6 +109,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'A generic Webhook action',
actionTypeId: '.webhook',
config: {

View file

@ -55,6 +55,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
objectRemover.add(space.id, response.body.id, 'action');
expect(response.body).to.eql({
id: response.body.id,
isPreconfigured: false,
name: 'My action',
actionTypeId: 'test.index-record',
config: {

View file

@ -137,6 +137,36 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it(`shouldn't delete action from preconfigured list`, async () => {
const response = await supertestWithoutAuth
.delete(`${getUrlPrefix(space.id)}/api/action/my-slack1`)
.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',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: 'Preconfigured action my-slack1 is not allowed to delete.',
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});

View file

@ -59,6 +59,7 @@ export default function getActionTests({ getService }: FtrProviderContext) {
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
id: createdAction.id,
isPreconfigured: false,
actionTypeId: 'test.index-record',
name: 'My action',
config: {
@ -115,6 +116,40 @@ export default function getActionTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle get preconfigured action request appropriately', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/api/action/my-slack1`)
.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: 'my-slack1',
actionTypeId: '.slack',
name: 'Slack#xyz',
isPreconfigured: true,
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});

View file

@ -10,11 +10,11 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/l
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function findActionTests({ getService }: FtrProviderContext) {
export default function getAllActionTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('find', () => {
describe('getAll', () => {
const objectRemover = new ObjectRemover(supertest);
afterEach(() => objectRemover.removeAll());
@ -22,7 +22,7 @@ export default function findActionTests({ getService }: FtrProviderContext) {
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle find action request appropriately', async () => {
it('should handle get all action request appropriately', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/action`)
.set('kbn-xsrf', 'foo')
@ -40,11 +40,7 @@ export default function findActionTests({ getService }: FtrProviderContext) {
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`
)
.get(`${getUrlPrefix(space.id)}/api/action/_getAll`)
.auth(user.username, user.password);
switch (scenario.id) {
@ -61,90 +57,47 @@ export default function findActionTests({ getService }: FtrProviderContext) {
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,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
referencedByCount: 0,
expect(response.body).to.eql([
{
id: createdAction.id,
isPreconfigured: false,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
],
});
referencedByCount: 0,
},
{
id: 'my-slack1',
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
referencedByCount: 0,
},
{
id: 'custom-system-abc-connector',
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
referencedByCount: 0,
},
]);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle find action request with filter appropriately', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/action`)
.set('kbn-xsrf', 'foo')
.send({
name: '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?filter=action.attributes.actionTypeId:test.index-record`
)
.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,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
referencedByCount: 0,
},
],
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle find request appropriately with proper referencedByCount', async () => {
it('should handle get all request appropriately with proper referencedByCount', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/action`)
.set('kbn-xsrf', 'foo')
@ -172,6 +125,13 @@ export default function findActionTests({ getService }: FtrProviderContext) {
id: createdAction.id,
params: {},
},
{
group: 'default',
id: 'my-slack1',
params: {
message: 'test',
},
},
],
})
)
@ -179,11 +139,7 @@ export default function findActionTests({ getService }: FtrProviderContext) {
objectRemover.add(space.id, createdAlert.id, 'alert');
const response = await supertestWithoutAuth
.get(
`${getUrlPrefix(
space.id
)}/api/action/_find?filter=action.attributes.actionTypeId:test.index-record`
)
.get(`${getUrlPrefix(space.id)}/api/action/_getAll`)
.auth(user.username, user.password);
switch (scenario.id) {
@ -200,29 +156,47 @@ export default function findActionTests({ getService }: FtrProviderContext) {
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,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
referencedByCount: 1,
expect(response.body).to.eql([
{
id: createdAction.id,
isPreconfigured: false,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
],
});
referencedByCount: 1,
},
{
id: 'my-slack1',
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
referencedByCount: 1,
},
{
id: 'custom-system-abc-connector',
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
referencedByCount: 0,
},
]);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it(`shouldn't find action from another space`, async () => {
it(`shouldn't get actions from another space`, async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/action`)
.set('kbn-xsrf', 'foo')
@ -240,11 +214,7 @@ export default function findActionTests({ getService }: FtrProviderContext) {
objectRemover.add(space.id, createdAction.id, 'action');
const response = await supertestWithoutAuth
.get(
`${getUrlPrefix(
'other'
)}/api/action/_find?search=test.index-record&search_fields=actionTypeId`
)
.get(`${getUrlPrefix('other')}/api/action/_getAll`)
.auth(user.username, user.password);
switch (scenario.id) {
@ -261,12 +231,30 @@ export default function findActionTests({ getService }: FtrProviderContext) {
case 'global_read at space1':
case 'superuser at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
page: 1,
perPage: 20,
total: 0,
data: [],
});
expect(response.body).to.eql([
{
id: 'my-slack1',
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
referencedByCount: 0,
},
{
id: 'custom-system-abc-connector',
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
referencedByCount: 0,
},
]);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);

View file

@ -19,7 +19,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./execute'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get_all'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./list_action_types'));
loadTestFile(require.resolve('./update'));

View file

@ -69,6 +69,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
id: createdAction.id,
isPreconfigured: false,
actionTypeId: 'test.index-record',
name: 'My action updated',
config: {
@ -307,6 +308,45 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it(`shouldn't update action from preconfigured list`, async () => {
const response = await supertestWithoutAuth
.put(`${getUrlPrefix(space.id)}/api/action/custom-system-abc-connector`)
.auth(user.username, user.password)
.set('kbn-xsrf', 'foo')
.send({
name: '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.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message: `Preconfigured action custom-system-abc-connector is not allowed to update.`,
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});

View file

@ -38,6 +38,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(createdAction).to.eql({
id: createdAction.id,
isPreconfigured: false,
name: 'An index action',
actionTypeId: '.index',
config: {
@ -55,6 +56,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(fetchedAction).to.eql({
id: fetchedAction.id,
isPreconfigured: false,
name: 'An index action',
actionTypeId: '.index',
config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null },
@ -77,6 +79,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(createdActionWithIndex).to.eql({
id: createdActionWithIndex.id,
isPreconfigured: false,
name: 'An index action with index config',
actionTypeId: '.index',
config: {
@ -94,6 +97,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
expect(fetchedActionWithIndex).to.eql({
id: fetchedActionWithIndex.id,
isPreconfigured: false,
name: 'An index action with index config',
actionTypeId: '.index',
config: {

View file

@ -37,6 +37,7 @@ export default function createActionTests({ getService }: FtrProviderContext) {
objectRemover.add(Spaces.space1.id, response.body.id, 'action');
expect(response.body).to.eql({
id: response.body.id,
isPreconfigured: false,
name: 'My action',
actionTypeId: 'test.index-record',
config: {

View file

@ -76,5 +76,16 @@ export default function deleteActionTests({ getService }: FtrProviderContext) {
message: 'Saved object [action/2] not found',
});
});
it(`shouldn't delete action from preconfigured list`, async () => {
await supertest
.delete(`${getUrlPrefix(Spaces.space1.id)}/api/action/my-slack1`)
.set('kbn-xsrf', 'foo')
.expect(400, {
statusCode: 400,
error: 'Bad Request',
message: `Preconfigured action my-slack1 is not allowed to delete.`,
});
});
});
}

View file

@ -1,92 +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 { Spaces } 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');
describe('find', () => {
const objectRemover = new ObjectRemover(supertest);
afterEach(() => objectRemover.removeAll());
it('should handle find action request appropriately', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/action`)
.set('kbn-xsrf', 'foo')
.send({
name: '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(Spaces.space1.id, createdAction.id, 'action');
await supertest
.get(
`${getUrlPrefix(
Spaces.space1.id
)}/api/action/_find?search=test.index-record&search_fields=actionTypeId`
)
.expect(200, {
page: 1,
perPage: 20,
total: 1,
data: [
{
id: createdAction.id,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
referencedByCount: 0,
},
],
});
});
it(`shouldn't find action from another space`, async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/action`)
.set('kbn-xsrf', 'foo')
.send({
name: '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(Spaces.space1.id, createdAction.id, 'action');
await supertest
.get(
`${getUrlPrefix(
Spaces.other.id
)}/api/action/_find?search=test.index-record&search_fields=actionTypeId`
)
.expect(200, {
page: 1,
perPage: 20,
total: 0,
data: [],
});
});
});
}

View file

@ -38,6 +38,7 @@ export default function getActionTests({ getService }: FtrProviderContext) {
.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/${createdAction.id}`)
.expect(200, {
id: createdAction.id,
isPreconfigured: false,
actionTypeId: 'test.index-record',
name: 'My action',
config: {
@ -71,5 +72,17 @@ export default function getActionTests({ getService }: FtrProviderContext) {
message: `Saved object [action/${createdAction.id}] not found`,
});
});
it('should handle get action request from preconfigured list', async () => {
await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/my-slack1`).expect(200, {
id: 'my-slack1',
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
});
});
});
}

View file

@ -0,0 +1,116 @@
/*
* 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 { Spaces } 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 getAllActionTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('getAll', () => {
const objectRemover = new ObjectRemover(supertest);
afterEach(() => objectRemover.removeAll());
it('should handle get all action request appropriately', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/action`)
.set('kbn-xsrf', 'foo')
.send({
name: '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(Spaces.space1.id, createdAction.id, 'action');
await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/action/_getAll`).expect(200, [
{
id: createdAction.id,
isPreconfigured: false,
name: 'My action',
actionTypeId: 'test.index-record',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
referencedByCount: 0,
},
{
id: 'my-slack1',
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
referencedByCount: 0,
},
{
id: 'custom-system-abc-connector',
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
referencedByCount: 0,
},
]);
});
it(`shouldn't get all action from another space`, async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/action`)
.set('kbn-xsrf', 'foo')
.send({
name: '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(Spaces.space1.id, createdAction.id, 'action');
await supertest.get(`${getUrlPrefix(Spaces.other.id)}/api/action/_getAll`).expect(200, [
{
id: 'my-slack1',
isPreconfigured: true,
actionTypeId: '.slack',
name: 'Slack#xyz',
config: {
webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz',
},
referencedByCount: 0,
},
{
id: 'custom-system-abc-connector',
isPreconfigured: true,
actionTypeId: 'system-abc-action-type',
name: 'SystemABC',
config: {
xyzConfig1: 'value1',
xyzConfig2: 'value2',
listOfThings: ['a', 'b', 'c', 'd'],
},
referencedByCount: 0,
},
]);
});
});
}

View file

@ -11,7 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) {
describe('Actions', () => {
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get_all'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./list_action_types'));
loadTestFile(require.resolve('./update'));

View file

@ -63,6 +63,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext)
actionTypeId: 'test.not-enabled',
config: {},
id: 'uuid-actionId',
isPreconfigured: false,
name: 'an action created before test.not-enabled was disabled',
});
});
@ -89,6 +90,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext)
actionTypeId: 'test.not-enabled',
config: {},
id: 'uuid-actionId',
isPreconfigured: false,
name: 'an action created before test.not-enabled was disabled',
});
});

View file

@ -48,6 +48,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
})
.expect(200, {
id: createdAction.id,
isPreconfigured: false,
actionTypeId: 'test.index-record',
name: 'My action updated',
config: {
@ -99,5 +100,25 @@ export default function updateActionTests({ getService }: FtrProviderContext) {
message: `Saved object [action/${createdAction.id}] not found`,
});
});
it(`shouldn't update action from preconfigured list`, async () => {
await supertest
.put(`${getUrlPrefix(Spaces.space1.id)}/api/action/custom-system-abc-connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action updated',
config: {
unencrypted: `This value shouldn't get encrypted`,
},
secrets: {
encrypted: 'This value should be encrypted',
},
})
.expect(400, {
statusCode: 400,
error: 'Bad Request',
message: `Preconfigured action custom-system-abc-connector is not allowed to update.`,
});
});
});
}