Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[actions] adds proxyBypassHosts and proxyOnlyHosts Kibana config keys #95365

Merged
merged 5 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/settings/alert-action-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ You can configure the following settings in the `kibana.yml` file.
| `xpack.actions.proxyUrl` {ess-icon}
| Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used.

| `xpack.actions.proxyBypassHosts` {ess-icon}
| Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time.

| `xpack.actions.proxyOnlyHosts` {ess-icon}
| Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time.

| `xpack.actions.proxyHeaders` {ess-icon}
| Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ kibana_vars=(
xpack.actions.proxyHeaders
xpack.actions.proxyRejectUnauthorizedCertificates
xpack.actions.proxyUrl
xpack.actions.proxyBypassHosts
xpack.actions.proxyOnlyHosts
xpack.actions.rejectUnauthorized
xpack.alerts.healthCheck.interval
xpack.alerts.invalidateApiKeysTask.interval
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ describe('create()', () => {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});

const localActionTypeRegistryParams = {
Expand Down
79 changes: 79 additions & 0 deletions x-pack/plugins/actions/server/actions_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,82 @@ describe('ensureActionTypeEnabled', () => {
expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined();
});
});

describe('getProxySettings', () => {
test('returns undefined when no proxy URL set', () => {
const config: ActionsConfig = {
...defaultActionsConfig,
proxyHeaders: { someHeaderName: 'some header value' },
proxyBypassHosts: ['avoid-proxy.co'],
};

const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
expect(proxySettings).toBeUndefined();
});

test('returns proxy url', () => {
const config: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
};
const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
expect(proxySettings?.proxyUrl).toBe(config.proxyUrl);
});

test('returns proxyRejectUnauthorizedCertificates', () => {
const configTrue: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyRejectUnauthorizedCertificates: true,
};
let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings();
expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true);

const configFalse: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyRejectUnauthorizedCertificates: false,
};
proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings();
expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false);
});

test('returns proxy headers', () => {
const proxyHeaders = {
someHeaderName: 'some header value',
someOtherHeader: 'some other header',
};
const config: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyHeaders,
};

const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
expect(proxySettings?.proxyHeaders).toEqual(config.proxyHeaders);
});

test('returns proxy bypass hosts', () => {
const proxyBypassHosts = ['proxy-bypass-1.elastic.co', 'proxy-bypass-2.elastic.co'];
const config: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyBypassHosts,
};

const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
expect(proxySettings?.proxyBypassHosts).toEqual(new Set(proxyBypassHosts));
});

test('returns proxy only hosts', () => {
const proxyOnlyHosts = ['proxy-only-1.elastic.co', 'proxy-only-2.elastic.co'];
const config: ActionsConfig = {
...defaultActionsConfig,
proxyUrl: 'https://proxy.elastic.co',
proxyOnlyHosts,
};

const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
expect(proxySettings?.proxyOnlyHosts).toEqual(new Set(proxyOnlyHosts));
});
});
17 changes: 9 additions & 8 deletions x-pack/plugins/actions/server/actions_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,11 @@ import url from 'url';
import { curry } from 'lodash';
import { pipe } from 'fp-ts/lib/pipeable';

import { ActionsConfig } from './config';
import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config';
import { ActionTypeDisabledError } from './lib';
import { ProxySettings } from './types';

export enum AllowedHosts {
Any = '*',
}

export enum EnabledActionTypes {
Any = '*',
}
export { AllowedHosts, EnabledActionTypes } from './config';

enum AllowListingField {
URL = 'url',
Expand Down Expand Up @@ -93,11 +87,18 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet

return {
proxyUrl: config.proxyUrl,
proxyBypassHosts: arrayAsSet(config.proxyBypassHosts),
proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts),
proxyHeaders: config.proxyHeaders,
proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates,
};
}

function arrayAsSet<T>(arr: T[] | undefined): Set<T> | undefined {
if (!arr) return;
return new Set(arr);
}

export function getActionsConfigurationUtilities(
config: ActionsConfig
): ActionsConfigurationUtilities {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

import axios from 'axios';
import { Agent as HttpsAgent } from 'https';
import HttpProxyAgent from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { Logger } from '../../../../../../src/core/server';
import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
import { getCustomAgents } from './get_custom_agents';

const TestUrl = 'https://elastic.co/foo/bar/baz';

const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const configurationUtilities = actionsConfigMock.create();
jest.mock('axios');
Expand Down Expand Up @@ -66,17 +70,19 @@ describe('request', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://localhost:1212',
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, TestUrl);

const res = await request({
axios,
url: 'http://testProxy',
url: TestUrl,
logger,
configurationUtilities,
});

expect(axiosMock).toHaveBeenCalledWith('http://testProxy', {
expect(axiosMock).toHaveBeenCalledWith(TestUrl, {
method: 'get',
data: {},
httpAgent,
Expand All @@ -94,6 +100,8 @@ describe('request', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope:',
proxyRejectUnauthorizedCertificates: false,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const res = await request({
axios,
Expand All @@ -116,6 +124,90 @@ describe('request', () => {
});
});

test('it bypasses with proxyBypassHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://elastic.proxy.co',
proxyBypassHosts: new Set(['elastic.co']),
proxyOnlyHosts: undefined,
});

await request({
axios,
url: TestUrl,
logger,
configurationUtilities,
});

expect(axiosMock.mock.calls.length).toBe(1);
const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
expect(httpAgent instanceof HttpProxyAgent).toBe(false);
expect(httpsAgent instanceof HttpsProxyAgent).toBe(false);
});

test('it does not bypass with proxyBypassHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://elastic.proxy.co',
proxyBypassHosts: new Set(['not-elastic.co']),
proxyOnlyHosts: undefined,
});

await request({
axios,
url: TestUrl,
logger,
configurationUtilities,
});

expect(axiosMock.mock.calls.length).toBe(1);
const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
expect(httpAgent instanceof HttpProxyAgent).toBe(true);
expect(httpsAgent instanceof HttpsProxyAgent).toBe(true);
});

test('it proxies with proxyOnlyHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://elastic.proxy.co',
proxyBypassHosts: undefined,
proxyOnlyHosts: new Set(['elastic.co']),
});

await request({
axios,
url: TestUrl,
logger,
configurationUtilities,
});

expect(axiosMock.mock.calls.length).toBe(1);
const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
expect(httpAgent instanceof HttpProxyAgent).toBe(true);
expect(httpsAgent instanceof HttpsProxyAgent).toBe(true);
});

test('it does not proxy with proxyOnlyHosts when expected', async () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://elastic.proxy.co',
proxyBypassHosts: undefined,
proxyOnlyHosts: new Set(['not-elastic.co']),
});

await request({
axios,
url: TestUrl,
logger,
configurationUtilities,
});

expect(axiosMock.mock.calls.length).toBe(1);
const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
expect(httpAgent instanceof HttpProxyAgent).toBe(false);
expect(httpsAgent instanceof HttpsProxyAgent).toBe(false);
});

test('it fetch correctly', async () => {
const res = await request({
axios,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const request = async <T = unknown>({
validateStatus?: (status: number) => boolean;
auth?: AxiosBasicCredentials;
}): Promise<AxiosResponse> => {
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url);

return await axios(url, {
...rest,
Expand Down
Loading