diff --git a/config/kibana.yml b/config/kibana.yml index 2351c3eb70d95f..7358fd5b06a36d 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -65,6 +65,10 @@ # headers, set this value to [] (an empty list). # elasticsearch.requestHeadersWhitelist: [ authorization ] +# Header names and values that are sent to Elasticsearch. Any custom headers cannot be overwritten +# by client-side headers, regardless of the elasticsearch.requestHeadersWhitelist configuration. +# elasticsearch.customHeaders: {} + # Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. # elasticsearch.shardTimeout: 0 diff --git a/docs/kibana-yml.asciidoc b/docs/kibana-yml.asciidoc index da630339c2fdd8..0a4e2a20d5a9ab 100644 --- a/docs/kibana-yml.asciidoc +++ b/docs/kibana-yml.asciidoc @@ -38,6 +38,8 @@ wait for Elasticsearch to respond to pings. Elasticsearch. This value must be a positive integer. `elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). +`elasticsearch.customHeaders:`:: *Default: `{}`* Header names and values to send to Elasticsearch. Any custom headers +cannot be overwritten by client-side headers, regardless of the `elasticsearch.requestHeadersWhitelist` configuration. `elasticsearch.shardTimeout:`:: *Default: 0* Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. `elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait for Elasticsearch at Kibana startup before diff --git a/src/core_plugins/elasticsearch/index.js b/src/core_plugins/elasticsearch/index.js index 7496e52403cb17..a536fa3f1dccf6 100644 --- a/src/core_plugins/elasticsearch/index.js +++ b/src/core_plugins/elasticsearch/index.js @@ -23,6 +23,7 @@ module.exports = function ({ Plugin }) { shardTimeout: number().default(0), requestTimeout: number().default(30000), requestHeadersWhitelist: array().items().single().default(DEFAULT_REQUEST_HEADERS), + customHeaders: object().default({}), pingTimeout: number().default(ref('requestTimeout')), startupTimeout: number().default(5000), ssl: object({ diff --git a/src/core_plugins/elasticsearch/lib/__tests__/map_uri.js b/src/core_plugins/elasticsearch/lib/__tests__/map_uri.js index 540ff912e7ba19..ca769ca3054f52 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/map_uri.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/map_uri.js @@ -22,15 +22,35 @@ describe('plugins/elasticsearch', function () { }; }); - it('only sends the whitelisted request headers', function () { + it('sends custom headers if set', function () { + const get = sinon.stub(); + get.withArgs('elasticsearch.requestHeadersWhitelist').returns([]); + get.withArgs('elasticsearch.customHeaders').returns({ foo: 'bar' }); + const server = { config: () => ({ get }) }; - const get = sinon.stub() - .withArgs('elasticsearch.url').returns('http://foobar:9200') - .withArgs('elasticsearch.requestHeadersWhitelist').returns(['x-my-custom-HEADER', 'Authorization']); - const config = function () { return { get: get }; }; - const server = { - config: config - }; + mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { + expect(err).to.be(null); + expect(upstreamHeaders).to.have.property('foo', 'bar'); + }); + }); + + it('sends configured custom headers even if the same named header exists in request', function () { + const get = sinon.stub(); + get.withArgs('elasticsearch.requestHeadersWhitelist').returns(['x-my-custom-header']); + get.withArgs('elasticsearch.customHeaders').returns({'x-my-custom-header': 'asconfigured'}); + const server = { config: () => ({ get }) }; + + mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { + expect(err).to.be(null); + expect(upstreamHeaders).to.have.property('x-my-custom-header', 'asconfigured'); + }); + }); + + it('only proxies the whitelisted request headers', function () { + const get = sinon.stub(); + get.withArgs('elasticsearch.requestHeadersWhitelist').returns(['x-my-custom-HEADER', 'Authorization']); + get.withArgs('elasticsearch.customHeaders').returns({}); + const server = { config: () => ({ get }) }; mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { expect(err).to.be(null); @@ -40,15 +60,11 @@ describe('plugins/elasticsearch', function () { }); }); - it('sends no headers if whitelist is set to []', function () { - - const get = sinon.stub() - .withArgs('elasticsearch.url').returns('http://foobar:9200') - .withArgs('elasticsearch.requestHeadersWhitelist').returns([]); - const config = function () { return { get: get }; }; - const server = { - config: config - }; + it('proxies no headers if whitelist is set to []', function () { + const get = sinon.stub(); + get.withArgs('elasticsearch.requestHeadersWhitelist').returns([]); + get.withArgs('elasticsearch.customHeaders').returns({}); + const server = { config: () => ({ get }) }; mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { expect(err).to.be(null); @@ -56,15 +72,11 @@ describe('plugins/elasticsearch', function () { }); }); - it('sends no headers if whitelist is set to no value', function () { - - const get = sinon.stub() - .withArgs('elasticsearch.url').returns('http://foobar:9200') - .withArgs('elasticsearch.requestHeadersWhitelist').returns([ null ]); // This is how Joi returns it - const config = function () { return { get: get }; }; - const server = { - config: config - }; + it('proxies no headers if whitelist is set to no value', function () { + const get = sinon.stub(); + get.withArgs('elasticsearch.requestHeadersWhitelist').returns([ null ]); // This is how Joi returns it + get.withArgs('elasticsearch.customHeaders').returns({}); + const server = { config: () => ({ get }) }; mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { expect(err).to.be(null); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/set_headers.js b/src/core_plugins/elasticsearch/lib/__tests__/set_headers.js new file mode 100644 index 00000000000000..0bed49f4d4c1cf --- /dev/null +++ b/src/core_plugins/elasticsearch/lib/__tests__/set_headers.js @@ -0,0 +1,39 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import setHeaders from '../set_headers'; + +describe('plugins/elasticsearch', function () { + describe('lib/set_headers', function () { + it('throws if not given an object as the first argument', function () { + const fn = () => setHeaders(null, {}); + expect(fn).to.throwError(); + }); + + it('throws if not given an object as the second argument', function () { + const fn = () => setHeaders({}, null); + expect(fn).to.throwError(); + }); + + it('returns a new object', function () { + const originalHeaders = {}; + const newHeaders = {}; + const returnedHeaders = setHeaders(originalHeaders, newHeaders); + expect(returnedHeaders).not.to.be(originalHeaders); + expect(returnedHeaders).not.to.be(newHeaders); + }); + + it('returns object with newHeaders merged with originalHeaders', function () { + const originalHeaders = { foo: 'bar' }; + const newHeaders = { one: 'two' }; + const returnedHeaders = setHeaders(originalHeaders, newHeaders); + expect(returnedHeaders).to.eql({ foo: 'bar', one: 'two' }); + }); + + it('returns object where newHeaders takes precedence for any matching keys', function () { + const originalHeaders = { foo: 'bar' }; + const newHeaders = { one: 'two', foo: 'notbar' }; + const returnedHeaders = setHeaders(originalHeaders, newHeaders); + expect(returnedHeaders).to.eql({ foo: 'notbar', one: 'two' }); + }); + }); +}); diff --git a/src/core_plugins/elasticsearch/lib/expose_client.js b/src/core_plugins/elasticsearch/lib/expose_client.js index 1bb4d5cfaebbb9..a1748e54ebbebe 100644 --- a/src/core_plugins/elasticsearch/lib/expose_client.js +++ b/src/core_plugins/elasticsearch/lib/expose_client.js @@ -55,9 +55,19 @@ module.exports = function (server) { ssl.ca = options.ca.map(readFile); } + const host = { + host: uri.hostname, + port: uri.port, + protocol: uri.protocol, + path: uri.pathname, + auth: uri.auth, + query: uri.query, + headers: config.get('elasticsearch.customHeaders') + }; + return new elasticsearch.Client({ - host: url.format(uri), - ssl: ssl, + host, + ssl, plugins: options.plugins, apiVersion: options.apiVersion, keepAlive: options.keepAlive, diff --git a/src/core_plugins/elasticsearch/lib/map_uri.js b/src/core_plugins/elasticsearch/lib/map_uri.js index 38068ffa35809c..53b7a8a4f38db5 100644 --- a/src/core_plugins/elasticsearch/lib/map_uri.js +++ b/src/core_plugins/elasticsearch/lib/map_uri.js @@ -1,8 +1,9 @@ import querystring from 'querystring'; import { resolve } from 'url'; import filterHeaders from './filter_headers'; +import setHeaders from './set_headers'; -module.exports = function mapUri(server, prefix) { +export default function mapUri(server, prefix) { const config = server.config(); return function (request, done) { @@ -14,7 +15,8 @@ module.exports = function mapUri(server, prefix) { } const query = querystring.stringify(request.query); if (query) url += '?' + query; - const filteredHeaders = filterHeaders(request.headers, server.config().get('elasticsearch.requestHeadersWhitelist')); - done(null, url, filteredHeaders); + const filteredHeaders = filterHeaders(request.headers, config.get('elasticsearch.requestHeadersWhitelist')); + const customHeaders = setHeaders(filteredHeaders, config.get('elasticsearch.customHeaders')); + done(null, url, customHeaders); }; }; diff --git a/src/core_plugins/elasticsearch/lib/set_headers.js b/src/core_plugins/elasticsearch/lib/set_headers.js new file mode 100644 index 00000000000000..8d9afba712d27c --- /dev/null +++ b/src/core_plugins/elasticsearch/lib/set_headers.js @@ -0,0 +1,15 @@ +import { isPlainObject } from 'lodash'; + +export default function setHeaders(originalHeaders, newHeaders) { + if (!isPlainObject(originalHeaders)) { + throw new Error(`Expected originalHeaders to be an object, but ${typeof originalHeaders} given`); + } + if (!isPlainObject(newHeaders)) { + throw new Error(`Expected newHeaders to be an object, but ${typeof newHeaders} given`); + } + + return { + ...originalHeaders, + ...newHeaders + }; +}