diff --git a/config/kibana.yml b/config/kibana.yml index 0e122d193dcf7..8694f65c61a5a 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -61,6 +61,10 @@ # must be a positive integer. # elasticsearch.requestTimeout: 30000 +# List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side +# headers, set this value to [] (an empty list). +# elasticsearch.requestHeadersWhitelist: [ authorization ] + # 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 fa4baf0ae4548..2ee6aa698756e 100644 --- a/docs/kibana-yml.asciidoc +++ b/docs/kibana-yml.asciidoc @@ -29,7 +29,9 @@ to `false`. wait for Elasticsearch to respond to pings. `elasticsearch.requestTimeout:`:: *Default: 300000* Time in milliseconds to wait for responses from the back end or Elasticsearch. This value must be a positive integer. -`elasticsearch.shardTimeout:`:: *Default: 0* Time in milliseconds for Elasticsearch to wait for responses from shards. Set +`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.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 retrying. diff --git a/docs/settings.asciidoc b/docs/settings.asciidoc index 0f105dc33e06d..1b99bec2a9307 100644 --- a/docs/settings.asciidoc +++ b/docs/settings.asciidoc @@ -376,6 +376,10 @@ deprecated[4.2, The names of several Kibana server properties changed in the 4.2 + *default*: `500000` +`elasticsearch.requestHeadersWhitelist:` added[5.0]:: List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). ++ +*default*: `[ 'authorization' ]` + `elasticsearch.shardTimeout` added[4.2]:: How long Elasticsearch should wait for responses from shards. Set to 0 to disable. + *alias*: `shard_timeout` deprecated[4.2] diff --git a/src/plugins/elasticsearch/index.js b/src/plugins/elasticsearch/index.js index e41c5113af600..7496e52403cb1 100644 --- a/src/plugins/elasticsearch/index.js +++ b/src/plugins/elasticsearch/index.js @@ -5,6 +5,8 @@ import healthCheck from './lib/health_check'; import exposeClient from './lib/expose_client'; import createProxy, { createPath } from './lib/create_proxy'; +const DEFAULT_REQUEST_HEADERS = [ 'authorization' ]; + module.exports = function ({ Plugin }) { return new Plugin({ require: ['kibana'], @@ -20,6 +22,7 @@ module.exports = function ({ Plugin }) { password: string(), shardTimeout: number().default(0), requestTimeout: number().default(30000), + requestHeadersWhitelist: array().items().single().default(DEFAULT_REQUEST_HEADERS), pingTimeout: number().default(ref('requestTimeout')), startupTimeout: number().default(5000), ssl: object({ diff --git a/src/plugins/elasticsearch/lib/__tests__/map_uri.js b/src/plugins/elasticsearch/lib/__tests__/map_uri.js new file mode 100644 index 0000000000000..540ff912e7ba1 --- /dev/null +++ b/src/plugins/elasticsearch/lib/__tests__/map_uri.js @@ -0,0 +1,76 @@ +import expect from 'expect.js'; +import mapUri from '../map_uri'; +import sinon from 'sinon'; + +describe('plugins/elasticsearch', function () { + describe('lib/map_uri', function () { + + let request; + + beforeEach(function () { + request = { + path: '/elasticsearch/some/path', + headers: { + cookie: 'some_cookie_string', + 'accept-encoding': 'gzip, deflate', + origin: 'https://localhost:5601', + 'content-type': 'application/json', + 'x-my-custom-header': '42', + accept: 'application/json, text/plain, */*', + authorization: '2343d322eda344390fdw42' + } + }; + }); + + it('only sends the whitelisted request headers', function () { + + 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('authorization'); + expect(upstreamHeaders).to.have.property('x-my-custom-header'); + expect(Object.keys(upstreamHeaders).length).to.be(2); + }); + }); + + 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 + }; + + mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { + expect(err).to.be(null); + expect(Object.keys(upstreamHeaders).length).to.be(0); + }); + }); + + 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 + }; + + mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) { + expect(err).to.be(null); + expect(Object.keys(upstreamHeaders).length).to.be(0); + }); + }); + + }); +}); diff --git a/src/plugins/elasticsearch/lib/call_with_request.js b/src/plugins/elasticsearch/lib/call_with_request.js index 385f3e61cc986..870b856de5fce 100644 --- a/src/plugins/elasticsearch/lib/call_with_request.js +++ b/src/plugins/elasticsearch/lib/call_with_request.js @@ -3,12 +3,12 @@ import Promise from 'bluebird'; import Boom from 'boom'; import getBasicAuthRealm from './get_basic_auth_realm'; import toPath from 'lodash/internal/toPath'; +import filterHeaders from './filter_headers'; -module.exports = (client) => { +module.exports = (server, client) => { return (req, endpoint, params = {}) => { - if (req.headers.authorization) { - _.set(params, 'headers.authorization', req.headers.authorization); - } + const filteredHeaders = filterHeaders(req.headers, server.config().get('elasticsearch.requestHeadersWhitelist')); + _.set(params, 'headers', filteredHeaders); const path = toPath(endpoint); const api = _.get(client, path); let apiContext = _.get(client, path.slice(0, -1)); diff --git a/src/plugins/elasticsearch/lib/create_proxy.js b/src/plugins/elasticsearch/lib/create_proxy.js index b2a82a07a1bca..2ef2bd1da56d8 100644 --- a/src/plugins/elasticsearch/lib/create_proxy.js +++ b/src/plugins/elasticsearch/lib/create_proxy.js @@ -16,10 +16,12 @@ function createProxy(server, method, route, config) { handler: { proxy: { mapUri: mapUri(server), - passThrough: true, agent: createAgent(server), xforward: true, - timeout: server.config().get('elasticsearch.requestTimeout') + timeout: server.config().get('elasticsearch.requestTimeout'), + onResponse: function (err, responseFromUpstream, request, reply) { + reply(err, responseFromUpstream); + } } }, }; diff --git a/src/plugins/elasticsearch/lib/expose_client.js b/src/plugins/elasticsearch/lib/expose_client.js index 0c81c07d25c7f..299b85bdd5c3d 100644 --- a/src/plugins/elasticsearch/lib/expose_client.js +++ b/src/plugins/elasticsearch/lib/expose_client.js @@ -78,8 +78,8 @@ module.exports = function (server) { server.expose('ElasticsearchClientLogging', ElasticsearchClientLogging); server.expose('client', client); server.expose('createClient', createClient); - server.expose('callWithRequestFactory', callWithRequest); - server.expose('callWithRequest', callWithRequest(noAuthClient)); + server.expose('callWithRequestFactory', _.partial(callWithRequest, server)); + server.expose('callWithRequest', callWithRequest(server, noAuthClient)); server.expose('errors', elasticsearch.errors); return client; diff --git a/src/plugins/elasticsearch/lib/filter_headers.js b/src/plugins/elasticsearch/lib/filter_headers.js new file mode 100644 index 0000000000000..285b45f560697 --- /dev/null +++ b/src/plugins/elasticsearch/lib/filter_headers.js @@ -0,0 +1,22 @@ +import _ from 'lodash'; + +export default function (originalHeaders, headersToKeep) { + + const normalizeHeader = function (header) { + if (!header) { + return ''; + } + header = header.toString(); + return header.trim().toLowerCase(); + }; + + // Normalize list of headers we want to allow in upstream request + const headersToKeepNormalized = headersToKeep.map(normalizeHeader); + + // Normalize original headers in request + const originalHeadersNormalized = _.mapKeys(originalHeaders, function (headerValue, headerName) { + return normalizeHeader(headerName); + }); + + return _.pick(originalHeaders, headersToKeepNormalized); +} diff --git a/src/plugins/elasticsearch/lib/map_uri.js b/src/plugins/elasticsearch/lib/map_uri.js index 79a2357f07086..38068ffa35809 100644 --- a/src/plugins/elasticsearch/lib/map_uri.js +++ b/src/plugins/elasticsearch/lib/map_uri.js @@ -1,6 +1,9 @@ import querystring from 'querystring'; import { resolve } from 'url'; +import filterHeaders from './filter_headers'; + module.exports = function mapUri(server, prefix) { + const config = server.config(); return function (request, done) { const path = request.path.replace('/elasticsearch', ''); @@ -11,6 +14,7 @@ module.exports = function mapUri(server, prefix) { } const query = querystring.stringify(request.query); if (query) url += '?' + query; - done(null, url); + const filteredHeaders = filterHeaders(request.headers, server.config().get('elasticsearch.requestHeadersWhitelist')); + done(null, url, filteredHeaders); }; };