adds per-actionType enablement via config xpack.actions.enabledActionTypes (#52967) (#53300)

* adds per-actionType enablement via config xpack.actions.enabledTypes

resolves: https://github.com/elastic/kibana/issues/52326
This commit is contained in:
Patrick Mueller 2019-12-17 12:27:04 -05:00 committed by GitHub
parent 435a906a4c
commit 6e620ed444
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 502 additions and 49 deletions

View file

@ -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[]

View file

@ -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

View file

@ -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();
},

View file

@ -12,6 +12,7 @@ const createActionTypeRegistryMock = () => {
register: jest.fn(),
get: jest.fn(),
list: jest.fn(),
ensureActionTypeEnabled: jest.fn(),
};
return mocked;
};

View file

@ -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,
},
]);
});

View file

@ -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),
}));
}
}

View file

@ -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()', () => {

View file

@ -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,

View file

@ -9,6 +9,8 @@ import { ActionsConfigurationUtilities } from './actions_config';
export const configUtilsMock: ActionsConfigurationUtilities = {
isWhitelistedHostname: _ => true,
isWhitelistedUri: _ => true,
isActionTypeEnabled: _ => true,
ensureWhitelistedHostname: _ => {},
ensureWhitelistedUri: _ => {},
ensureActionTypeEnabled: _ => {},
};

View file

@ -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();
});
});

View file

@ -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));
}
},
};
}

View file

@ -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 = () => {};

View file

@ -22,6 +22,7 @@ export function createActionTypeRegistry(): {
const actionTypeRegistry = new ActionTypeRegistry({
taskManager: taskManagerMock.create(),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
actionsConfigUtils: configUtilsMock,
});
registerBuiltInActionTypes({
logger,

View file

@ -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",
}
`);
});

View file

@ -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 {

View file

@ -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

View file

@ -23,6 +23,7 @@ it('calls the list function', async () => {
{
id: '1',
name: 'One',
enabled: true,
},
];

View file

@ -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>;
},
},

View file

@ -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

View file

@ -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')}`,

View file

@ -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',

View file

@ -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',
});
});
});

View file

@ -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',
});
});
});

View file

@ -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 () => {

View file

@ -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)}`);
}
});
});
}
});

View file

@ -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'));
});
}

View file

@ -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);
});
});
}

View 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": {}
}
}
}
}