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:
parent
18c3f75bfb
commit
730dcbf638
|
@ -25,6 +25,7 @@ exports[`Step1 editing should allow for editing 1`] = `
|
|||
"actionTypeId": "1abc",
|
||||
"config": Object {},
|
||||
"id": "1",
|
||||
"isPreconfigured": false,
|
||||
"name": "Testing",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`,
|
||||
{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 = () => ({
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -20,4 +20,5 @@ export interface ActionResult {
|
|||
actionTypeId: string;
|
||||
name: string;
|
||||
config: Record<string, any>;
|
||||
isPreconfigured: boolean;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 [
|
||||
"*",
|
||||
],
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 } });
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ const createStartMock = () => {
|
|||
execute: jest.fn(),
|
||||
isActionTypeEnabled: jest.fn(),
|
||||
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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!),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
117
x-pack/plugins/actions/server/routes/get_all.test.ts
Normal file
117
x-pack/plugins/actions/server/routes/get_all.test.ts
Normal 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);
|
||||
});
|
||||
});
|
42
x-pack/plugins/actions/server/routes/get_all.ts
Normal file
42
x-pack/plugins/actions/server/routes/get_all.ts
Normal 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,
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -30,6 +30,7 @@ const alertsClientParams = {
|
|||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingServiceMock.create().get(),
|
||||
encryptedSavedObjectsPlugin: encryptedSavedObjects,
|
||||
preconfiguredActions: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,6 +218,7 @@ export class AlertingPlugin {
|
|||
getSpaceId(request: KibanaRequest) {
|
||||
return spaces?.getSpaceId(request);
|
||||
},
|
||||
preconfiguredActions: plugins.actions.preconfiguredActions,
|
||||
});
|
||||
|
||||
taskRunnerFactory.initialize({
|
||||
|
|
|
@ -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')]);
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ describe('connector reducer', () => {
|
|||
actionTypeId: 'test-action-type-id',
|
||||
name: 'action-connector',
|
||||
referencedByCount: 0,
|
||||
isPreconfigured: false,
|
||||
config: {},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 [
|
||||
{
|
||||
|
|
|
@ -66,6 +66,7 @@ export interface ActionConnector {
|
|||
name: string;
|
||||
referencedByCount?: number;
|
||||
config: Record<string, any>;
|
||||
isPreconfigured: boolean;
|
||||
}
|
||||
|
||||
export type ActionConnectorWithoutId = Omit<ActionConnector, 'id'>;
|
||||
|
|
|
@ -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')}`,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)}`);
|
|
@ -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'));
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue