[actions] adds proxyBypassHosts and proxyOnlyHosts Kibana config keys (#95365)
resolves https://github.com/elastic/kibana/issues/92949 This PR adds two new Kibana config keys to further customize when the proxy is used when making HTTP requests. Prior to this PR, if a proxy was set via the `xpack.actions.proxyUrl` config key, all requests would be proxied. Now, there's a further refinement in that hostnames can be added to the `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` config keys. Only one of these config keys can be used at a time. If the target URL hostname of the HTTP request is listed in the `proxyBypassHosts` list, the proxy won't be used. If the target URL hostname of the HTTP request is **NOT** listed in the `proxyOnlyHosts` list, the proxy won't be used. Depending on the customer's environment, it may be easier to list the hosts to bypass, or easier to list the hosts that should only be proxied, so they can choose either method.
This commit is contained in:
parent
21f38afd27
commit
ad06d16beb
|
@ -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 {}.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -406,6 +406,8 @@ describe('create()', () => {
|
|||
preconfigured: {},
|
||||
proxyRejectUnauthorizedCertificates: true,
|
||||
rejectUnauthorized: true,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
|
||||
const localActionTypeRegistryParams = {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
|
@ -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,
|
||||
|
@ -94,6 +100,8 @@ describe('request', () => {
|
|||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: ':nope:',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const res = await request({
|
||||
axios,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -14,6 +14,10 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
|||
import { actionsConfigMock } from '../../actions_config.mock';
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
const targetHost = 'elastic.co';
|
||||
const targetUrl = `https://${targetHost}/foo/bar/baz`;
|
||||
const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`;
|
||||
|
||||
describe('getCustomAgents', () => {
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
|
||||
|
@ -21,8 +25,10 @@ describe('getCustomAgents', () => {
|
|||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
|
||||
expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
|
||||
expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
|
||||
});
|
||||
|
@ -31,15 +37,73 @@ describe('getCustomAgents', () => {
|
|||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: ':nope: not a valid URL',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
|
||||
expect(httpAgent).toBe(undefined);
|
||||
expect(httpsAgent instanceof HttpsAgent).toBeTruthy();
|
||||
});
|
||||
|
||||
test('return default agents for undefined proxy options', () => {
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
|
||||
expect(httpAgent).toBe(undefined);
|
||||
expect(httpsAgent instanceof HttpsAgent).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns non-proxy agents for matching proxyBypassHosts', () => {
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: new Set([targetHost]),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
|
||||
expect(httpAgent instanceof HttpProxyAgent).toBeFalsy();
|
||||
expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy();
|
||||
});
|
||||
|
||||
test('returns proxy agents for non-matching proxyBypassHosts', () => {
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: new Set([targetHost]),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(
|
||||
configurationUtilities,
|
||||
logger,
|
||||
nonMatchingUrl
|
||||
);
|
||||
expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
|
||||
expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns proxy agents for matching proxyOnlyHosts', () => {
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set([targetHost]),
|
||||
});
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
|
||||
expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
|
||||
expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
|
||||
});
|
||||
|
||||
test('returns non-proxy agents for non-matching proxyOnlyHosts', () => {
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set([targetHost]),
|
||||
});
|
||||
const { httpAgent, httpsAgent } = getCustomAgents(
|
||||
configurationUtilities,
|
||||
logger,
|
||||
nonMatchingUrl
|
||||
);
|
||||
expect(httpAgent instanceof HttpProxyAgent).toBeFalsy();
|
||||
expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,8 @@ interface GetCustomAgentsResponse {
|
|||
|
||||
export function getCustomAgents(
|
||||
configurationUtilities: ActionsConfigurationUtilities,
|
||||
logger: Logger
|
||||
logger: Logger,
|
||||
url: string
|
||||
): GetCustomAgentsResponse {
|
||||
const proxySettings = configurationUtilities.getProxySettings();
|
||||
const defaultAgents = {
|
||||
|
@ -33,6 +34,28 @@ export function getCustomAgents(
|
|||
return defaultAgents;
|
||||
}
|
||||
|
||||
let targetUrl: URL;
|
||||
try {
|
||||
targetUrl = new URL(url);
|
||||
} catch (err) {
|
||||
logger.warn(`error determining proxy state for invalid url "${url}", using default agents`);
|
||||
return defaultAgents;
|
||||
}
|
||||
|
||||
// filter out hostnames in the proxy bypass or only lists
|
||||
const { hostname } = targetUrl;
|
||||
|
||||
if (proxySettings.proxyBypassHosts) {
|
||||
if (proxySettings.proxyBypassHosts.has(hostname)) {
|
||||
return defaultAgents;
|
||||
}
|
||||
}
|
||||
|
||||
if (proxySettings.proxyOnlyHosts) {
|
||||
if (!proxySettings.proxyOnlyHosts.has(hostname)) {
|
||||
return defaultAgents;
|
||||
}
|
||||
}
|
||||
logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`);
|
||||
let proxyUrl: URL;
|
||||
try {
|
||||
|
|
|
@ -76,6 +76,8 @@ describe('send_email module', () => {
|
|||
{
|
||||
proxyUrl: 'https://example.com',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -222,6 +224,138 @@ describe('send_email module', () => {
|
|||
|
||||
await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops');
|
||||
});
|
||||
|
||||
test('it bypasses with proxyBypassHosts when expected', async () => {
|
||||
const sendEmailOptions = getSendEmailOptionsNoAuth(
|
||||
{
|
||||
transport: {
|
||||
host: 'example.com',
|
||||
port: 1025,
|
||||
},
|
||||
},
|
||||
{
|
||||
proxyUrl: 'https://proxy.com',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: new Set(['example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"host": "example.com",
|
||||
"port": 1025,
|
||||
"secure": false,
|
||||
"tls": Object {
|
||||
"rejectUnauthorized": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it does not bypass with proxyBypassHosts when expected', async () => {
|
||||
const sendEmailOptions = getSendEmailOptionsNoAuth(
|
||||
{
|
||||
transport: {
|
||||
host: 'example.com',
|
||||
port: 1025,
|
||||
},
|
||||
},
|
||||
{
|
||||
proxyUrl: 'https://proxy.com',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: new Set(['not-example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"headers": undefined,
|
||||
"host": "example.com",
|
||||
"port": 1025,
|
||||
"proxy": "https://proxy.com",
|
||||
"secure": false,
|
||||
"tls": Object {
|
||||
"rejectUnauthorized": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it proxies with proxyOnlyHosts when expected', async () => {
|
||||
const sendEmailOptions = getSendEmailOptionsNoAuth(
|
||||
{
|
||||
transport: {
|
||||
host: 'example.com',
|
||||
port: 1025,
|
||||
},
|
||||
},
|
||||
{
|
||||
proxyUrl: 'https://proxy.com',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['example.com']),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"headers": undefined,
|
||||
"host": "example.com",
|
||||
"port": 1025,
|
||||
"proxy": "https://proxy.com",
|
||||
"secure": false,
|
||||
"tls": Object {
|
||||
"rejectUnauthorized": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('it does not proxy with proxyOnlyHosts when expected', async () => {
|
||||
const sendEmailOptions = getSendEmailOptionsNoAuth(
|
||||
{
|
||||
transport: {
|
||||
host: 'example.com',
|
||||
port: 1025,
|
||||
},
|
||||
},
|
||||
{
|
||||
proxyUrl: 'https://proxy.com',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['not-example.com']),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await sendEmail(mockLogger, sendEmailOptions);
|
||||
expect(result).toBe(sendMailMockResult);
|
||||
expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"host": "example.com",
|
||||
"port": 1025,
|
||||
"secure": false,
|
||||
"tls": Object {
|
||||
"rejectUnauthorized": false,
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
function getSendEmailOptions(
|
||||
|
|
|
@ -63,6 +63,17 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
|
|||
};
|
||||
}
|
||||
|
||||
let useProxy = !!proxySettings;
|
||||
|
||||
if (host) {
|
||||
if (proxySettings?.proxyBypassHosts && proxySettings?.proxyBypassHosts?.has(host)) {
|
||||
useProxy = false;
|
||||
}
|
||||
if (proxySettings?.proxyOnlyHosts && !proxySettings?.proxyOnlyHosts?.has(host)) {
|
||||
useProxy = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (service === JSON_TRANSPORT_SERVICE) {
|
||||
transportConfig.jsonTransport = true;
|
||||
delete transportConfig.auth;
|
||||
|
@ -73,7 +84,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
|
|||
transportConfig.port = port;
|
||||
transportConfig.secure = !!secure;
|
||||
|
||||
if (proxySettings) {
|
||||
if (proxySettings && useProxy) {
|
||||
transportConfig.tls = {
|
||||
// do not fail on invalid certs if value is false
|
||||
rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates,
|
||||
|
|
|
@ -195,6 +195,8 @@ describe('execute()', () => {
|
|||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const actionTypeProxy = getActionType({
|
||||
logger: mockedLogger,
|
||||
|
@ -212,6 +214,106 @@ describe('execute()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('ensure proxy bypass will bypass when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: new Set(['example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const actionTypeProxy = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities,
|
||||
});
|
||||
await actionTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy bypass will not bypass when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: new Set(['not-example.com']),
|
||||
proxyOnlyHosts: undefined,
|
||||
});
|
||||
const actionTypeProxy = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities,
|
||||
});
|
||||
await actionTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy only will proxy when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['example.com']),
|
||||
});
|
||||
const actionTypeProxy = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities,
|
||||
});
|
||||
await actionTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
});
|
||||
expect(mockedLogger.debug).toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('ensure proxy only will not proxy when expected', async () => {
|
||||
mockedLogger.debug.mockReset();
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
configurationUtilities.getProxySettings.mockReturnValue({
|
||||
proxyUrl: 'https://someproxyhost',
|
||||
proxyRejectUnauthorizedCertificates: false,
|
||||
proxyBypassHosts: undefined,
|
||||
proxyOnlyHosts: new Set(['not-example.com']),
|
||||
});
|
||||
const actionTypeProxy = getActionType({
|
||||
logger: mockedLogger,
|
||||
configurationUtilities,
|
||||
});
|
||||
await actionTypeProxy.executor({
|
||||
actionId: 'some-id',
|
||||
services,
|
||||
config: {},
|
||||
secrets: { webhookUrl: 'http://example.com' },
|
||||
params: { message: 'this invocation should succeed' },
|
||||
});
|
||||
expect(mockedLogger.debug).not.toHaveBeenCalledWith(
|
||||
'IncomingWebhook was called with proxyUrl https://someproxyhost'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders parameter templates as expected', async () => {
|
||||
expect(actionType.renderParameterTemplates).toBeTruthy();
|
||||
const paramsWithTemplates = {
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { URL } from 'url';
|
||||
import { curry } from 'lodash';
|
||||
import HttpProxyAgent from 'http-proxy-agent';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
|
||||
|
@ -131,13 +133,15 @@ async function slackExecutor(
|
|||
const { message } = params;
|
||||
const proxySettings = configurationUtilities.getProxySettings();
|
||||
|
||||
const customAgents = getCustomAgents(configurationUtilities, logger);
|
||||
const customAgents = getCustomAgents(configurationUtilities, logger, webhookUrl);
|
||||
const agent = webhookUrl.toLowerCase().startsWith('https')
|
||||
? customAgents.httpsAgent
|
||||
: customAgents.httpAgent;
|
||||
|
||||
if (proxySettings) {
|
||||
logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`);
|
||||
if (agent instanceof HttpProxyAgent || agent instanceof HttpsProxyAgent) {
|
||||
logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -5,9 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { configSchema } from './config';
|
||||
import { configSchema, ActionsConfig, getValidatedConfig } from './config';
|
||||
import { Logger } from '../../../..//src/core/server';
|
||||
import { loggingSystemMock } from '../../../..//src/core/server/mocks';
|
||||
|
||||
const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
|
||||
describe('config validation', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('action defaults', () => {
|
||||
const config: Record<string, unknown> = {};
|
||||
expect(configSchema.validate(config)).toMatchInlineSnapshot(`
|
||||
|
@ -84,6 +92,56 @@ describe('config validation', () => {
|
|||
`"[preconfigured]: invalid preconfigured action id \\"__proto__\\""`
|
||||
);
|
||||
});
|
||||
|
||||
test('validates proxyBypassHosts and proxyOnlyHosts', () => {
|
||||
const bypassHosts = ['bypass.elastic.co'];
|
||||
const onlyHosts = ['only.elastic.co'];
|
||||
let validated: ActionsConfig;
|
||||
|
||||
validated = configSchema.validate({});
|
||||
expect(validated.proxyBypassHosts).toEqual(undefined);
|
||||
expect(validated.proxyOnlyHosts).toEqual(undefined);
|
||||
|
||||
validated = configSchema.validate({
|
||||
proxyBypassHosts: bypassHosts,
|
||||
});
|
||||
expect(validated.proxyBypassHosts).toEqual(bypassHosts);
|
||||
expect(validated.proxyOnlyHosts).toEqual(undefined);
|
||||
|
||||
validated = configSchema.validate({
|
||||
proxyOnlyHosts: onlyHosts,
|
||||
});
|
||||
expect(validated.proxyBypassHosts).toEqual(undefined);
|
||||
expect(validated.proxyOnlyHosts).toEqual(onlyHosts);
|
||||
});
|
||||
|
||||
test('validates proxyBypassHosts and proxyOnlyHosts used at the same time', () => {
|
||||
const bypassHosts = ['bypass.elastic.co'];
|
||||
const onlyHosts = ['only.elastic.co'];
|
||||
const config: Record<string, unknown> = {
|
||||
proxyBypassHosts: bypassHosts,
|
||||
proxyOnlyHosts: onlyHosts,
|
||||
};
|
||||
|
||||
let validated: ActionsConfig;
|
||||
|
||||
// the config schema validation validates with both set
|
||||
validated = configSchema.validate(config);
|
||||
expect(validated.proxyBypassHosts).toEqual(bypassHosts);
|
||||
expect(validated.proxyOnlyHosts).toEqual(onlyHosts);
|
||||
|
||||
// getValidatedConfig will warn and set onlyHosts to undefined with both set
|
||||
validated = getValidatedConfig(mockLogger, validated);
|
||||
expect(validated.proxyBypassHosts).toEqual(bypassHosts);
|
||||
expect(validated.proxyOnlyHosts).toEqual(undefined);
|
||||
expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
// object creator that ensures we can create a property named __proto__ on an
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { AllowedHosts, EnabledActionTypes } from './actions_config';
|
||||
import { Logger } from '../../../../src/core/server';
|
||||
|
||||
export enum AllowedHosts {
|
||||
Any = '*',
|
||||
}
|
||||
|
||||
export enum EnabledActionTypes {
|
||||
Any = '*',
|
||||
}
|
||||
|
||||
const preconfiguredActionSchema = schema.object({
|
||||
name: schema.string({ minLength: 1 }),
|
||||
|
@ -36,11 +44,32 @@ export const configSchema = schema.object({
|
|||
proxyUrl: schema.maybe(schema.string()),
|
||||
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
|
||||
proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
|
||||
proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
|
||||
rejectUnauthorized: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
|
||||
export type ActionsConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
// It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on
|
||||
// simultaneous usage in the config validator directly, but there's no good way to express
|
||||
// this relationship in the cloud config constraints, so we're doing it "live".
|
||||
export function getValidatedConfig(logger: Logger, originalConfig: ActionsConfig): ActionsConfig {
|
||||
const proxyBypassHosts = originalConfig.proxyBypassHosts;
|
||||
const proxyOnlyHosts = originalConfig.proxyOnlyHosts;
|
||||
|
||||
if (proxyBypassHosts && proxyOnlyHosts) {
|
||||
logger.warn(
|
||||
'The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.'
|
||||
);
|
||||
const tmp: Record<string, unknown> = originalConfig;
|
||||
delete tmp.proxyOnlyHosts;
|
||||
return tmp as ActionsConfig;
|
||||
}
|
||||
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
const invalidActionIds = new Set(['', '__proto__', 'constructor']);
|
||||
|
||||
function validatePreconfigured(preconfigured: Record<string, unknown>): string | undefined {
|
||||
|
|
|
@ -30,7 +30,7 @@ import { SpacesPluginStart } from '../../spaces/server';
|
|||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
|
||||
import { ActionsConfig } from './config';
|
||||
import { ActionsConfig, getValidatedConfig } from './config';
|
||||
import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib';
|
||||
import { ActionsClient } from './actions_client';
|
||||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
|
@ -141,8 +141,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
private readonly kibanaIndexConfig: { kibana: { index: string } };
|
||||
|
||||
constructor(initContext: PluginInitializerContext) {
|
||||
this.actionsConfig = initContext.config.get<ActionsConfig>();
|
||||
this.logger = initContext.logger.get('actions');
|
||||
this.actionsConfig = getValidatedConfig(this.logger, initContext.config.get<ActionsConfig>());
|
||||
this.telemetryLogger = initContext.logger.get('usage');
|
||||
this.preconfiguredActions = [];
|
||||
this.kibanaIndexConfig = initContext.config.legacy.get();
|
||||
|
|
|
@ -133,6 +133,8 @@ export interface ActionTaskExecutorParams {
|
|||
|
||||
export interface ProxySettings {
|
||||
proxyUrl: string;
|
||||
proxyBypassHosts: Set<string> | undefined;
|
||||
proxyOnlyHosts: Set<string> | undefined;
|
||||
proxyHeaders?: Record<string, string>;
|
||||
proxyRejectUnauthorizedCertificates: boolean;
|
||||
}
|
||||
|
|
|
@ -68,12 +68,24 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
|
||||
const proxyPort =
|
||||
process.env.ALERTING_PROXY_PORT ?? (await getPort({ port: getPort.makeRange(6200, 6300) }));
|
||||
|
||||
// If testing with proxy, also test proxyOnlyHosts for this proxy;
|
||||
// all the actions are assumed to be acccessing localhost anyway.
|
||||
// If not testing with proxy, set a bogus proxy up, and set the bypass
|
||||
// flag for all our localhost actions to bypass it. Currently,
|
||||
// security_and_spaces uses enableActionsProxy: true, and spaces_only
|
||||
// uses enableActionsProxy: false.
|
||||
const proxyHosts = ['localhost', 'some.non.existent.com'];
|
||||
const actionsProxyUrl = options.enableActionsProxy
|
||||
? [
|
||||
`--xpack.actions.proxyUrl=http://localhost:${proxyPort}`,
|
||||
`--xpack.actions.proxyOnlyHosts=${JSON.stringify(proxyHosts)}`,
|
||||
'--xpack.actions.proxyRejectUnauthorizedCertificates=false',
|
||||
]
|
||||
: [];
|
||||
: [
|
||||
`--xpack.actions.proxyUrl=http://elastic.co`,
|
||||
`--xpack.actions.proxyBypassHosts=${JSON.stringify(proxyHosts)}`,
|
||||
];
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve(`../${name}/tests/`)],
|
||||
|
|
Loading…
Reference in a new issue