* adds per-actionType enablement via config xpack.actions.enabledTypes resolves: https://github.com/elastic/kibana/issues/52326
This commit is contained in:
parent
435a906a4c
commit
6e620ed444
|
@ -396,12 +396,16 @@ Rollup user interface.
|
|||
|
||||
`i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`.
|
||||
|
||||
`xpack.actions.enabledActionTypes:`:: *Default: +[ {asterisk} ]+* Set this value
|
||||
to an array of action types that are enabled. An element of `*` indicates all
|
||||
action types registered are enabled. The action types provided by Kibana are:
|
||||
`.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`.
|
||||
|
||||
`xpack.actions.whitelistedHosts:`:: *Default: +[ {asterisk} ]+* Set this value
|
||||
to an array of host names which actions such as email, slack, pagerduty, and
|
||||
webhook can connect to. An element of `*` indicates any host can be connected
|
||||
to. An empty array indicates no hosts can be connected to.
|
||||
|
||||
|
||||
include::{docdir}/settings/apm-settings.asciidoc[]
|
||||
include::{docdir}/settings/dev-settings.asciidoc[]
|
||||
include::{docdir}/settings/graph-settings.asciidoc[]
|
||||
|
|
|
@ -34,6 +34,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> |
|
||||
|
||||
#### Whitelisting Built-in Action Types
|
||||
It is worth noting that the **whitelistedHosts** configuation applies to built-in action types (such as Slack, or PagerDuty) as well.
|
||||
|
@ -49,8 +50,10 @@ This module provides a Utilities for interacting with the configuration.
|
|||
| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean |
|
||||
| isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean |
|
||||
| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean |
|
||||
| ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted |
|
||||
| ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted |
|
||||
| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled |
|
||||
|
||||
## Action types
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Legacy } from 'kibana';
|
|||
import { Root } from 'joi';
|
||||
import mappings from './mappings.json';
|
||||
import { init } from './server';
|
||||
import { WhitelistedHosts, EnabledActionTypes } from './server/actions_config';
|
||||
|
||||
export {
|
||||
ActionsPlugin,
|
||||
|
@ -38,10 +39,14 @@ export function actions(kibana: any) {
|
|||
.items(
|
||||
Joi.string()
|
||||
.hostname()
|
||||
.allow('*')
|
||||
.allow(WhitelistedHosts.Any)
|
||||
)
|
||||
.sparse(false)
|
||||
.default(['*']),
|
||||
.default([WhitelistedHosts.Any]),
|
||||
enabledActionTypes: Joi.array()
|
||||
.items(Joi.string())
|
||||
.sparse(false)
|
||||
.default([EnabledActionTypes.Any]),
|
||||
})
|
||||
.default();
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ const createActionTypeRegistryMock = () => {
|
|||
register: jest.fn(),
|
||||
get: jest.fn(),
|
||||
list: jest.fn(),
|
||||
ensureActionTypeEnabled: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -8,11 +8,13 @@ import { taskManagerMock } from '../../task_manager/task_manager.mock';
|
|||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
import { ExecutorType } from './types';
|
||||
import { ActionExecutor, ExecutorError, TaskRunnerFactory } from './lib';
|
||||
import { configUtilsMock } from './actions_config.mock';
|
||||
|
||||
const mockTaskManager = taskManagerMock.create();
|
||||
const actionTypeRegistryParams = {
|
||||
taskManager: mockTaskManager,
|
||||
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
|
||||
actionsConfigUtils: configUtilsMock,
|
||||
};
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
@ -123,6 +125,7 @@ describe('list()', () => {
|
|||
{
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -10,20 +10,23 @@ import { TaskManagerSetupContract } from './shim';
|
|||
import { RunContext } from '../../task_manager';
|
||||
import { ExecutorError, TaskRunnerFactory } from './lib';
|
||||
import { ActionType } from './types';
|
||||
|
||||
import { ActionsConfigurationUtilities } from './actions_config';
|
||||
interface ConstructorOptions {
|
||||
taskManager: TaskManagerSetupContract;
|
||||
taskRunnerFactory: TaskRunnerFactory;
|
||||
actionsConfigUtils: ActionsConfigurationUtilities;
|
||||
}
|
||||
|
||||
export class ActionTypeRegistry {
|
||||
private readonly taskManager: TaskManagerSetupContract;
|
||||
private readonly actionTypes: Map<string, ActionType> = new Map();
|
||||
private readonly taskRunnerFactory: TaskRunnerFactory;
|
||||
private readonly actionsConfigUtils: ActionsConfigurationUtilities;
|
||||
|
||||
constructor({ taskManager, taskRunnerFactory }: ConstructorOptions) {
|
||||
this.taskManager = taskManager;
|
||||
this.taskRunnerFactory = taskRunnerFactory;
|
||||
constructor(constructorParams: ConstructorOptions) {
|
||||
this.taskManager = constructorParams.taskManager;
|
||||
this.taskRunnerFactory = constructorParams.taskRunnerFactory;
|
||||
this.actionsConfigUtils = constructorParams.actionsConfigUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,6 +36,13 @@ export class ActionTypeRegistry {
|
|||
return this.actionTypes.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error if action type is not enabled.
|
||||
*/
|
||||
public ensureActionTypeEnabled(id: string) {
|
||||
this.actionsConfigUtils.ensureActionTypeEnabled(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an action type to the action type registry
|
||||
*/
|
||||
|
@ -86,12 +96,13 @@ export class ActionTypeRegistry {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a list of registered action types [{ id, name }]
|
||||
* Returns a list of registered action types [{ id, name, enabled }]
|
||||
*/
|
||||
public list() {
|
||||
return Array.from(this.actionTypes).map(([actionTypeId, actionType]) => ({
|
||||
id: actionTypeId,
|
||||
name: actionType.name,
|
||||
enabled: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ import { ActionsClient } from './actions_client';
|
|||
import { ExecutorType } from './types';
|
||||
import { ActionExecutor, TaskRunnerFactory } from './lib';
|
||||
import { taskManagerMock } from '../../task_manager/task_manager.mock';
|
||||
import { configUtilsMock } from './actions_config.mock';
|
||||
import { getActionsConfigurationUtilities } from './actions_config';
|
||||
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
savedObjectsClientMock,
|
||||
|
@ -25,6 +28,7 @@ const mockTaskManager = taskManagerMock.create();
|
|||
const actionTypeRegistryParams = {
|
||||
taskManager: mockTaskManager,
|
||||
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
|
||||
actionsConfigUtils: configUtilsMock,
|
||||
};
|
||||
|
||||
let actionsClient: ActionsClient;
|
||||
|
@ -190,6 +194,58 @@ describe('create()', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws error creating action with disabled actionType', async () => {
|
||||
const localConfigUtils = getActionsConfigurationUtilities({
|
||||
enabled: true,
|
||||
enabledActionTypes: ['some-not-ignored-action-type'],
|
||||
whitelistedHosts: ['*'],
|
||||
});
|
||||
|
||||
const localActionTypeRegistryParams = {
|
||||
taskManager: mockTaskManager,
|
||||
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
|
||||
actionsConfigUtils: localConfigUtils,
|
||||
};
|
||||
|
||||
actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams);
|
||||
actionsClient = new ActionsClient({
|
||||
actionTypeRegistry,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient,
|
||||
defaultKibanaIndex,
|
||||
});
|
||||
|
||||
const savedObjectCreateResult = {
|
||||
id: '1',
|
||||
type: 'type',
|
||||
attributes: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
actionTypeRegistry.register({
|
||||
id: 'my-action-type',
|
||||
name: 'My action type',
|
||||
executor,
|
||||
});
|
||||
savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult);
|
||||
|
||||
await expect(
|
||||
actionsClient.create({
|
||||
action: {
|
||||
name: 'my name',
|
||||
actionTypeId: 'my-action-type',
|
||||
config: {},
|
||||
secrets: {},
|
||||
},
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"action type \\"my-action-type\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import {
|
||||
IScopedClusterClient,
|
||||
SavedObjectsClientContract,
|
||||
|
@ -92,6 +93,12 @@ export class ActionsClient {
|
|||
const validatedActionTypeConfig = validateConfig(actionType, config);
|
||||
const validatedActionTypeSecrets = validateSecrets(actionType, secrets);
|
||||
|
||||
try {
|
||||
this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
} catch (err) {
|
||||
throw Boom.badRequest(err.message);
|
||||
}
|
||||
|
||||
const result = await this.savedObjectsClient.create('action', {
|
||||
actionTypeId,
|
||||
name,
|
||||
|
|
|
@ -9,6 +9,8 @@ import { ActionsConfigurationUtilities } from './actions_config';
|
|||
export const configUtilsMock: ActionsConfigurationUtilities = {
|
||||
isWhitelistedHostname: _ => true,
|
||||
isWhitelistedUri: _ => true,
|
||||
isActionTypeEnabled: _ => true,
|
||||
ensureWhitelistedHostname: _ => {},
|
||||
ensureWhitelistedUri: _ => {},
|
||||
ensureActionTypeEnabled: _ => {},
|
||||
};
|
||||
|
|
|
@ -5,13 +5,24 @@
|
|||
*/
|
||||
|
||||
import { ActionsConfigType } from './types';
|
||||
import { getActionsConfigurationUtilities, WhitelistedHosts } from './actions_config';
|
||||
import {
|
||||
getActionsConfigurationUtilities,
|
||||
WhitelistedHosts,
|
||||
EnabledActionTypes,
|
||||
} from './actions_config';
|
||||
|
||||
const DefaultActionsConfig: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
|
||||
describe('ensureWhitelistedUri', () => {
|
||||
test('returns true when "any" hostnames are allowed', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [WhitelistedHosts.Any],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedUri(
|
||||
|
@ -21,27 +32,31 @@ describe('ensureWhitelistedUri', () => {
|
|||
});
|
||||
|
||||
test('throws when the hostname in the requested uri is not in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(() =>
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedUri(
|
||||
'https://github.com/elastic/kibana'
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"target url \\"https://github.com/elastic/kibana\\" is not in the Kibana whitelist"`
|
||||
`"target url \\"https://github.com/elastic/kibana\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when the uri cannot be parsed as a valid URI', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(() =>
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedUri('github.com/elastic')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"target url \\"github.com/elastic\\" is not in the Kibana whitelist"`
|
||||
`"target url \\"github.com/elastic\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"`
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true when the hostname in the requested uri is in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] };
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: ['github.com'],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedUri(
|
||||
'https://github.com/elastic/kibana'
|
||||
|
@ -55,6 +70,7 @@ describe('ensureWhitelistedHostname', () => {
|
|||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [WhitelistedHosts.Any],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com')
|
||||
|
@ -62,16 +78,20 @@ describe('ensureWhitelistedHostname', () => {
|
|||
});
|
||||
|
||||
test('throws when the hostname in the requested uri is not in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(() =>
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"target hostname \\"github.com\\" is not in the Kibana whitelist"`
|
||||
`"target hostname \\"github.com\\" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts"`
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true when the hostname in the requested uri is in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] };
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: ['github.com'],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com')
|
||||
).toBeUndefined();
|
||||
|
@ -83,6 +103,7 @@ describe('isWhitelistedUri', () => {
|
|||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [WhitelistedHosts.Any],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana')
|
||||
|
@ -90,21 +111,25 @@ describe('isWhitelistedUri', () => {
|
|||
});
|
||||
|
||||
test('throws when the hostname in the requested uri is not in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana')
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('throws when the uri cannot be parsed as a valid URI', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(getActionsConfigurationUtilities(config).isWhitelistedUri('github.com/elastic')).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true when the hostname in the requested uri is in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] };
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: ['github.com'],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(
|
||||
getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana')
|
||||
).toEqual(true);
|
||||
|
@ -116,6 +141,7 @@ describe('isWhitelistedHostname', () => {
|
|||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [WhitelistedHosts.Any],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual(
|
||||
true
|
||||
|
@ -123,16 +149,100 @@ describe('isWhitelistedHostname', () => {
|
|||
});
|
||||
|
||||
test('throws when the hostname in the requested uri is not in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true when the hostname in the requested uri is in the whitelist', () => {
|
||||
const config: ActionsConfigType = { enabled: false, whitelistedHosts: ['github.com'] };
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: ['github.com'],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isActionTypeEnabled', () => {
|
||||
test('returns true when "any" actionTypes are allowed', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: ['ignore', EnabledActionTypes.Any],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(true);
|
||||
});
|
||||
|
||||
test('returns false when no actionType is allowed', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: [],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(false);
|
||||
});
|
||||
|
||||
test('returns false when the actionType is not in the enabled list', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: ['foo'],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('bar')).toEqual(false);
|
||||
});
|
||||
|
||||
test('returns true when the actionType is in the enabled list', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: ['ignore', 'foo'],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).isActionTypeEnabled('foo')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureActionTypeEnabled', () => {
|
||||
test('does not throw when any actionType is allowed', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: ['ignore', EnabledActionTypes.Any],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('throws when no actionType is not allowed', () => {
|
||||
const config: ActionsConfigType = DefaultActionsConfig;
|
||||
expect(() =>
|
||||
getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"action type \\"foo\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when actionType is not enabled', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: ['ignore'],
|
||||
};
|
||||
expect(() =>
|
||||
getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"action type \\"foo\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"`
|
||||
);
|
||||
});
|
||||
|
||||
test('does not throw when actionType is enabled', () => {
|
||||
const config: ActionsConfigType = {
|
||||
enabled: false,
|
||||
whitelistedHosts: [],
|
||||
enabledActionTypes: ['ignore', 'foo'],
|
||||
};
|
||||
expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { tryCatch, fromNullable, isSome, map, mapNullable, getOrElse } from 'fp-ts/lib/Option';
|
||||
import { tryCatch, map, mapNullable, getOrElse } from 'fp-ts/lib/Option';
|
||||
import { URL } from 'url';
|
||||
import { curry } from 'lodash';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
@ -16,6 +16,10 @@ export enum WhitelistedHosts {
|
|||
Any = '*',
|
||||
}
|
||||
|
||||
export enum EnabledActionTypes {
|
||||
Any = '*',
|
||||
}
|
||||
|
||||
enum WhitelistingField {
|
||||
url = 'url',
|
||||
hostname = 'hostname',
|
||||
|
@ -24,13 +28,16 @@ enum WhitelistingField {
|
|||
export interface ActionsConfigurationUtilities {
|
||||
isWhitelistedHostname: (hostname: string) => boolean;
|
||||
isWhitelistedUri: (uri: string) => boolean;
|
||||
isActionTypeEnabled: (actionType: string) => boolean;
|
||||
ensureWhitelistedHostname: (hostname: string) => void;
|
||||
ensureWhitelistedUri: (uri: string) => void;
|
||||
ensureActionTypeEnabled: (actionType: string) => void;
|
||||
}
|
||||
|
||||
function whitelistingErrorMessage(field: WhitelistingField, value: string) {
|
||||
return i18n.translate('xpack.actions.urlWhitelistConfigurationError', {
|
||||
defaultMessage: 'target {field} "{value}" is not in the Kibana whitelist',
|
||||
defaultMessage:
|
||||
'target {field} "{value}" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts',
|
||||
values: {
|
||||
value,
|
||||
field,
|
||||
|
@ -38,22 +45,21 @@ function whitelistingErrorMessage(field: WhitelistingField, value: string) {
|
|||
});
|
||||
}
|
||||
|
||||
function doesValueWhitelistAnyHostname(whitelistedHostname: string): boolean {
|
||||
return whitelistedHostname === WhitelistedHosts.Any;
|
||||
function disabledActionTypeErrorMessage(actionType: string) {
|
||||
return i18n.translate('xpack.actions.disabledActionTypeError', {
|
||||
defaultMessage:
|
||||
'action type "{actionType}" is not enabled in the Kibana config xpack.actions.enabledActionTypes',
|
||||
values: {
|
||||
actionType,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isWhitelisted({ whitelistedHosts }: ActionsConfigType, hostname: string): boolean {
|
||||
return (
|
||||
Array.isArray(whitelistedHosts) &&
|
||||
isSome(
|
||||
fromNullable(
|
||||
whitelistedHosts.find(
|
||||
whitelistedHostname =>
|
||||
doesValueWhitelistAnyHostname(whitelistedHostname) || whitelistedHostname === hostname
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const whitelisted = new Set(whitelistedHosts);
|
||||
if (whitelisted.has(WhitelistedHosts.Any)) return true;
|
||||
if (whitelisted.has(hostname)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boolean {
|
||||
|
@ -65,14 +71,26 @@ function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boo
|
|||
);
|
||||
}
|
||||
|
||||
function isActionTypeEnabledInConfig(
|
||||
{ enabledActionTypes }: ActionsConfigType,
|
||||
actionType: string
|
||||
): boolean {
|
||||
const enabled = new Set(enabledActionTypes);
|
||||
if (enabled.has(EnabledActionTypes.Any)) return true;
|
||||
if (enabled.has(actionType)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getActionsConfigurationUtilities(
|
||||
config: ActionsConfigType
|
||||
): ActionsConfigurationUtilities {
|
||||
const isWhitelistedHostname = curry(isWhitelisted)(config);
|
||||
const isWhitelistedUri = curry(isWhitelistedHostnameInUri)(config);
|
||||
const isActionTypeEnabled = curry(isActionTypeEnabledInConfig)(config);
|
||||
return {
|
||||
isWhitelistedHostname,
|
||||
isWhitelistedUri,
|
||||
isActionTypeEnabled,
|
||||
ensureWhitelistedUri(uri: string) {
|
||||
if (!isWhitelistedUri(uri)) {
|
||||
throw new Error(whitelistingErrorMessage(WhitelistingField.url, uri));
|
||||
|
@ -83,5 +101,10 @@ export function getActionsConfigurationUtilities(
|
|||
throw new Error(whitelistingErrorMessage(WhitelistingField.hostname, hostname));
|
||||
}
|
||||
},
|
||||
ensureActionTypeEnabled(actionType: string) {
|
||||
if (!isActionTypeEnabled(actionType)) {
|
||||
throw new Error(disabledActionTypeErrorMessage(actionType));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Logger } from '../../../../../../src/core/server';
|
|||
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
|
||||
|
||||
import { ActionType, ActionTypeExecutorOptions } from '../types';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { configUtilsMock } from '../actions_config.mock';
|
||||
import { validateConfig, validateSecrets, validateParams } from '../lib';
|
||||
import { createActionTypeRegistry } from './index.test';
|
||||
import { sendEmail } from './lib/send_email';
|
||||
|
@ -25,13 +25,6 @@ import {
|
|||
|
||||
const sendEmailMock = sendEmail as jest.Mock;
|
||||
|
||||
const configUtilsMock: ActionsConfigurationUtilities = {
|
||||
isWhitelistedHostname: _ => true,
|
||||
isWhitelistedUri: _ => true,
|
||||
ensureWhitelistedHostname: _ => {},
|
||||
ensureWhitelistedUri: _ => {},
|
||||
};
|
||||
|
||||
const ACTION_TYPE_ID = '.email';
|
||||
const NO_OP_FN = () => {};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ export function createActionTypeRegistry(): {
|
|||
const actionTypeRegistry = new ActionTypeRegistry({
|
||||
taskManager: taskManagerMock.create(),
|
||||
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
|
||||
actionsConfigUtils: configUtilsMock,
|
||||
});
|
||||
registerBuiltInActionTypes({
|
||||
logger,
|
||||
|
|
|
@ -202,3 +202,36 @@ test('throws an error when failing to load action through savedObjectsClient', a
|
|||
`"No access"`
|
||||
);
|
||||
});
|
||||
|
||||
test('returns an error if actionType is not enabled', async () => {
|
||||
const actionType = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
executor: jest.fn(),
|
||||
};
|
||||
const actionSavedObject = {
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
},
|
||||
references: [],
|
||||
};
|
||||
savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject);
|
||||
encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject);
|
||||
actionTypeRegistry.get.mockReturnValueOnce(actionType);
|
||||
actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => {
|
||||
throw new Error('not enabled for test');
|
||||
});
|
||||
const result = await actionExecutor.execute(executeParams);
|
||||
|
||||
expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test');
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actionId": "1",
|
||||
"message": "not enabled for test",
|
||||
"retry": false,
|
||||
"status": "error",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -69,6 +69,13 @@ export class ActionExecutor {
|
|||
const {
|
||||
attributes: { actionTypeId, config, name },
|
||||
} = await services.savedObjectsClient.get<RawAction>('action', actionId);
|
||||
|
||||
try {
|
||||
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
|
||||
} catch (err) {
|
||||
return { status: 'error', actionId, message: err.message, retry: false };
|
||||
}
|
||||
|
||||
// Only get encrypted attributes here, the remaining attributes can be fetched in
|
||||
// the savedObjectsClient call
|
||||
const {
|
||||
|
|
|
@ -85,9 +85,11 @@ export class Plugin {
|
|||
|
||||
const actionExecutor = new ActionExecutor();
|
||||
const taskRunnerFactory = new TaskRunnerFactory(actionExecutor);
|
||||
const actionsConfigUtils = getActionsConfigurationUtilities(config as ActionsConfigType);
|
||||
const actionTypeRegistry = new ActionTypeRegistry({
|
||||
taskRunnerFactory,
|
||||
taskManager: plugins.task_manager,
|
||||
actionsConfigUtils,
|
||||
});
|
||||
this.taskRunnerFactory = taskRunnerFactory;
|
||||
this.actionTypeRegistry = actionTypeRegistry;
|
||||
|
@ -97,7 +99,7 @@ export class Plugin {
|
|||
registerBuiltInActionTypes({
|
||||
logger: this.logger,
|
||||
actionTypeRegistry,
|
||||
actionsConfigUtils: getActionsConfigurationUtilities(config as ActionsConfigType),
|
||||
actionsConfigUtils,
|
||||
});
|
||||
|
||||
// Routes
|
||||
|
|
|
@ -23,6 +23,7 @@ it('calls the list function', async () => {
|
|||
{
|
||||
id: '1',
|
||||
name: 'One',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ export function shim(
|
|||
return Rx.of({
|
||||
enabled: server.config().get('xpack.actions.enabled') as boolean,
|
||||
whitelistedHosts: server.config().get('xpack.actions.whitelistedHosts') as string[],
|
||||
enabledActionTypes: server.config().get('xpack.actions.enabledActionTypes') as string[],
|
||||
}) as Rx.Observable<ActionsConfigType>;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface ActionsPlugin {
|
|||
export interface ActionsConfigType {
|
||||
enabled: boolean;
|
||||
whitelistedHosts: string[];
|
||||
enabledActionTypes: string[];
|
||||
}
|
||||
|
||||
// the parameters passed to an action type executor function
|
||||
|
|
|
@ -16,6 +16,21 @@ interface CreateTestConfigOptions {
|
|||
ssl?: boolean;
|
||||
}
|
||||
|
||||
// test.not-enabled is specifically not enabled
|
||||
const enabledActionTypes = [
|
||||
'.server-log',
|
||||
'.slack',
|
||||
'.email',
|
||||
'.index',
|
||||
'.pagerduty',
|
||||
'.webhook',
|
||||
'test.noop',
|
||||
'test.index-record',
|
||||
'test.failing',
|
||||
'test.rate-limit',
|
||||
'test.authorization',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export function createTestConfig(name: string, options: CreateTestConfigOptions) {
|
||||
const { license = 'trial', disabledPlugins = [], ssl = false } = options;
|
||||
|
@ -57,6 +72,8 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
'localhost',
|
||||
'some.non.existent.com',
|
||||
])}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
'--xpack.alerting.enabled=true',
|
||||
...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
|
||||
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import Hapi from 'hapi';
|
||||
import { ActionType } from '../../../../../../legacy/plugins/actions';
|
||||
|
||||
import { initPlugin as initSlack } from './slack_simulation';
|
||||
import { initPlugin as initWebhook } from './webhook_simulation';
|
||||
import { initPlugin as initPagerduty } from './pagerduty_simulation';
|
||||
|
@ -32,6 +34,15 @@ export default function(kibana: any) {
|
|||
require: ['actions'],
|
||||
name: NAME,
|
||||
init: (server: Hapi.Server) => {
|
||||
// this action is specifically NOT enabled in ../../config.ts
|
||||
const notEnabledActionType: ActionType = {
|
||||
id: 'test.not-enabled',
|
||||
name: 'Test: Not Enabled',
|
||||
async executor() {
|
||||
return { status: 'ok', actionId: '' };
|
||||
},
|
||||
};
|
||||
server.plugins.actions!.setup.registerType(notEnabledActionType);
|
||||
server.plugins.xpack_main.registerFeature({
|
||||
id: 'actions',
|
||||
name: 'Actions',
|
||||
|
|
|
@ -111,7 +111,7 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: error configuring pagerduty action: target url "https://events.pagerduty.com/v2/enqueue" is not in the Kibana whitelist',
|
||||
'error validating action type config: error configuring pagerduty action: target url "https://events.pagerduty.com/v2/enqueue" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -103,7 +103,7 @@ export default function slackTest({ getService }: FtrProviderContext) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: error configuring slack action: target url "http://slack.mynonexistent.com" is not in the Kibana whitelist',
|
||||
'error validating action type secrets: error configuring slack action: target url "http://slack.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -180,7 +180,7 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
.expect(400);
|
||||
|
||||
expect(result.error).to.eql('Bad Request');
|
||||
expect(result.message).to.match(/not in the Kibana whitelist/);
|
||||
expect(result.message).to.match(/is not whitelisted in the Kibana config/);
|
||||
});
|
||||
|
||||
it('should handle unreachable webhook targets', async () => {
|
||||
|
|
|
@ -179,6 +179,42 @@ export default function createActionTests({ getService }: FtrProviderContext) {
|
|||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should handle create action requests for action types that are not enabled`, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send({
|
||||
name: 'my name',
|
||||
actionTypeId: 'test.not-enabled',
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(404);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 404,
|
||||
error: 'Not Found',
|
||||
message: 'Not Found',
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,5 +17,6 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./update'));
|
||||
loadTestFile(require.resolve('./execute'));
|
||||
loadTestFile(require.resolve('./builtin_action_types/es_index'));
|
||||
loadTestFile(require.resolve('./type_not_enabled'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
const PREWRITTEN_ACTION_ID = 'uuid-actionId';
|
||||
const DISABLED_ACTION_TYPE = 'test.not-enabled';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function typeNotEnabledTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('actionType not enabled', () => {
|
||||
// loads action PREWRITTEN_ACTION_ID with actionType DISABLED_ACTION_TYPE
|
||||
before(() => esArchiver.load('alerting'));
|
||||
after(() => esArchiver.unload('alerting'));
|
||||
|
||||
it('should handle create action with disabled actionType request appropriately', async () => {
|
||||
const response = await supertest
|
||||
.post(`/api/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My action',
|
||||
actionTypeId: DISABLED_ACTION_TYPE,
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(400);
|
||||
expect(response.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should handle execute request with disabled actionType appropriately`, async () => {
|
||||
const response = await supertest
|
||||
.post(`/api/action/${PREWRITTEN_ACTION_ID}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
status: 'error',
|
||||
retry: false,
|
||||
actionId: PREWRITTEN_ACTION_ID,
|
||||
message:
|
||||
'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle get action request with disabled actionType appropriately', async () => {
|
||||
const response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`);
|
||||
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
actionTypeId: 'test.not-enabled',
|
||||
config: {},
|
||||
id: 'uuid-actionId',
|
||||
name: 'an action created before test.not-enabled was disabled',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle update action request with disabled actionType appropriately', async () => {
|
||||
const responseUpdate = await supertest
|
||||
.put(`/api/action/${PREWRITTEN_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'an action created before test.not-enabled was disabled (updated)',
|
||||
});
|
||||
|
||||
expect(responseUpdate.statusCode).to.eql(200);
|
||||
expect(responseUpdate.body).to.eql({
|
||||
actionTypeId: 'test.not-enabled',
|
||||
config: {},
|
||||
id: 'uuid-actionId',
|
||||
name: 'an action created before test.not-enabled was disabled (updated)',
|
||||
});
|
||||
|
||||
const response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.eql({
|
||||
actionTypeId: 'test.not-enabled',
|
||||
config: {},
|
||||
id: 'uuid-actionId',
|
||||
name: 'an action created before test.not-enabled was disabled (updated)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle delete action request with disabled actionType appropriately', async () => {
|
||||
let response;
|
||||
|
||||
response = await supertest
|
||||
.delete(`/api/action/${PREWRITTEN_ACTION_ID}`)
|
||||
.set('kbn-xsrf', 'foo');
|
||||
expect(response.statusCode).to.eql(204);
|
||||
|
||||
response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`);
|
||||
expect(response.statusCode).to.eql(404);
|
||||
});
|
||||
});
|
||||
}
|
15
x-pack/test/functional/es_archives/alerting/data.json
Normal file
15
x-pack/test/functional/es_archives/alerting/data.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"id": "action:uuid-actionId",
|
||||
"source": {
|
||||
"type": "action",
|
||||
"action": {
|
||||
"actionTypeId": "test.not-enabled",
|
||||
"name": "an action created before test.not-enabled was disabled",
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue