Skip to content

Commit

Permalink
Merge pull request #6896 from ycombinator/gh-6484
Browse files Browse the repository at this point in the history
Only proxy whitelisted request headers to ES server upstream
  • Loading branch information
ycombinator committed Apr 19, 2016
2 parents 21a00be + 028db3f commit aa2f65f
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 10 deletions.
4 changes: 4 additions & 0 deletions config/kibana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion docs/kibana-yml.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions docs/settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/elasticsearch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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({
Expand Down
76 changes: 76 additions & 0 deletions src/plugins/elasticsearch/lib/__tests__/map_uri.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

});
});
8 changes: 4 additions & 4 deletions src/plugins/elasticsearch/lib/call_with_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
6 changes: 4 additions & 2 deletions src/plugins/elasticsearch/lib/create_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
},
};
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/elasticsearch/lib/expose_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/plugins/elasticsearch/lib/filter_headers.js
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 5 additions & 1 deletion src/plugins/elasticsearch/lib/map_uri.js
Original file line number Diff line number Diff line change
@@ -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', '');
Expand All @@ -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);
};
};

0 comments on commit aa2f65f

Please sign in to comment.