Skip to content

Commit

Permalink
adds per-actionType enablement via config xpack.actions.enabledTypes
Browse files Browse the repository at this point in the history
resolves: elastic#52326
  • Loading branch information
pmuellr committed Dec 12, 2019
1 parent 0cd5bb0 commit d47cd6e
Show file tree
Hide file tree
Showing 19 changed files with 144 additions and 42 deletions.
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export function actions(kibana: any) {
)
.sparse(false)
.default([]),
enabledTypes: Joi.array()
.items(Joi.string())
.sparse(false)
.default(['*']),
})
.default();
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const createActionTypeRegistryMock = () => {
register: jest.fn(),
get: jest.fn(),
list: jest.fn(),
ensureActionTypeEnabled: jest.fn(),
};
return mocked;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getMockActionConfig } from './actions_config.mock';

const mockTaskManager = taskManagerMock.create();
const actionTypeRegistryParams = {
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
actionsConfigUtils: getMockActionConfig(),
};

beforeEach(() => jest.resetAllMocks());
Expand Down Expand Up @@ -123,6 +125,7 @@ describe('list()', () => {
{
id: 'my-action-type',
name: 'My action type',
enabled: true,
},
]);
});
Expand Down
21 changes: 16 additions & 5 deletions x-pack/legacy/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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
*/
Expand Down Expand Up @@ -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),
}));
}
}
2 changes: 2 additions & 0 deletions x-pack/legacy/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ActionsClient } from './actions_client';
import { ExecutorType } from './types';
import { ActionExecutor, TaskRunnerFactory } from './lib';
import { taskManagerMock } from '../../task_manager/task_manager.mock';
import { getMockActionConfig } from './actions_config.mock';
import {
elasticsearchServiceMock,
savedObjectsClientMock,
Expand All @@ -25,6 +26,7 @@ const mockTaskManager = taskManagerMock.create();
const actionTypeRegistryParams = {
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()),
actionsConfigUtils: getMockActionConfig(),
};

let actionsClient: ActionsClient;
Expand Down
7 changes: 7 additions & 0 deletions x-pack/legacy/plugins/actions/server/actions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import Boom from 'boom';
import {
IScopedClusterClient,
SavedObjectsClientContract,
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions x-pack/legacy/plugins/actions/server/actions_config.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { ActionsConfigurationUtilities } from './actions_config';

const MOCK_KIBANA_CONFIG_UTILS: ActionsConfigurationUtilities = {
isWhitelistedHostname: _ => true,
isWhitelistedUri: _ => true,
isActionTypeEnabled: _ => true,
ensureWhitelistedHostname: _ => {},
ensureWhitelistedUri: _ => {},
ensureActionTypeEnabled: _ => {},
};

export function getMockActionConfig(): ActionsConfigurationUtilities {
return MOCK_KIBANA_CONFIG_UTILS;
}
46 changes: 33 additions & 13 deletions x-pack/legacy/plugins/actions/server/actions_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('ensureWhitelistedUri', () => {
const config: ActionsConfigType = {
enabled: false,
whitelistedHosts: [WhitelistedHosts.Any],
enabledTypes: [],
};
expect(
getActionsConfigurationUtilities(config).ensureWhitelistedUri(
Expand All @@ -21,27 +22,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 = { enabled: false, whitelistedHosts: [], enabledTypes: [] };
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 = { enabled: false, whitelistedHosts: [], enabledTypes: [] };
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'],
enabledTypes: [],
};
expect(
getActionsConfigurationUtilities(config).ensureWhitelistedUri(
'https://github.com/elastic/kibana'
Expand All @@ -55,23 +60,28 @@ describe('ensureWhitelistedHostname', () => {
const config: ActionsConfigType = {
enabled: false,
whitelistedHosts: [WhitelistedHosts.Any],
enabledTypes: [],
};
expect(
getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com')
).toBeUndefined();
});

test('throws when the hostname in the requested uri is not in the whitelist', () => {
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [], enabledTypes: [] };
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'],
enabledTypes: [],
};
expect(
getActionsConfigurationUtilities(config).ensureWhitelistedHostname('github.com')
).toBeUndefined();
Expand All @@ -83,28 +93,33 @@ describe('isWhitelistedUri', () => {
const config: ActionsConfigType = {
enabled: false,
whitelistedHosts: [WhitelistedHosts.Any],
enabledTypes: [],
};
expect(
getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana')
).toEqual(true);
});

test('throws when the hostname in the requested uri is not in the whitelist', () => {
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [], enabledTypes: [] };
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 = { enabled: false, whitelistedHosts: [], enabledTypes: [] };
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'],
enabledTypes: [],
};
expect(
getActionsConfigurationUtilities(config).isWhitelistedUri('https://github.com/elastic/kibana')
).toEqual(true);
Expand All @@ -116,21 +131,26 @@ describe('isWhitelistedHostname', () => {
const config: ActionsConfigType = {
enabled: false,
whitelistedHosts: [WhitelistedHosts.Any],
enabledTypes: [],
};
expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual(
true
);
});

test('throws when the hostname in the requested uri is not in the whitelist', () => {
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [] };
const config: ActionsConfigType = { enabled: false, whitelistedHosts: [], enabledTypes: [] };
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'],
enabledTypes: [],
};
expect(getActionsConfigurationUtilities(config).isWhitelistedHostname('github.com')).toEqual(
true
);
Expand Down
32 changes: 31 additions & 1 deletion x-pack/legacy/plugins/actions/server/actions_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,33 @@ 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,
},
});
}

function disabledActionTypeErrorMessage(actionType: string) {
return i18n.translate('xpack.actions.urlWhitelistConfigurationError', {
defaultMessage:
'action type "{actionType}" is not enabled in the Kibana config xpack.actions.enabledTypes',
values: {
actionType,
},
});
}

function doesValueWhitelistAnyHostname(whitelistedHostname: string): boolean {
return whitelistedHostname === WhitelistedHosts.Any;
}
Expand Down Expand Up @@ -65,14 +78,26 @@ function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boo
);
}

function isActionTypeEnabledinConfig(
{ enabledTypes }: ActionsConfigType,
actionType: string
): boolean {
const enabled = new Set(enabledTypes);
if (enabled.has('*')) 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));
Expand All @@ -83,5 +108,10 @@ export function getActionsConfigurationUtilities(
throw new Error(whitelistingErrorMessage(WhitelistingField.hostname, hostname));
}
},
ensureActionTypeEnabled(actionType: string) {
if (!isActionTypeEnabled(actionType)) {
throw new Error(disabledActionTypeErrorMessage(actionType));
}
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getMockActionConfig } from '../actions_config.mock';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { createActionTypeRegistry } from './index.test';
import { sendEmail } from './lib/send_email';
Expand All @@ -25,12 +25,7 @@ import {

const sendEmailMock = sendEmail as jest.Mock;

const configUtilsMock: ActionsConfigurationUtilities = {
isWhitelistedHostname: _ => true,
isWhitelistedUri: _ => true,
ensureWhitelistedHostname: _ => {},
ensureWhitelistedUri: _ => {},
};
const configUtilsMock = getMockActionConfig();

const ACTION_TYPE_ID = '.email';
const NO_OP_FN = () => {};
Expand Down
Loading

0 comments on commit d47cd6e

Please sign in to comment.