diff --git a/src/core_plugins/elasticsearch/lib/__tests__/cluster.js b/src/core_plugins/elasticsearch/lib/__tests__/cluster.js index 10f6ce07335df8..f06f9fb848a986 100644 --- a/src/core_plugins/elasticsearch/lib/__tests__/cluster.js +++ b/src/core_plugins/elasticsearch/lib/__tests__/cluster.js @@ -113,6 +113,35 @@ describe('plugins/elasticsearch', function () { headers: headers }); }); + + describe('wrap401Errors', () => { + let handler; + let error; + + beforeEach(() => { + error = new Error('Authentication required'); + error.statusCode = 401; + + handler = sinon.stub(); + }); + + it('ensures WWW-Authenticate header', async () => { + set(client, 'mock.401', sinon.stub().returns(Promise.reject(error))); + await cluster.callWithRequest({}, 'mock.401', {}, { wrap401Errors: true }).catch(handler); + + sinon.assert.calledOnce(handler); + expect(handler.getCall(0).args[0].output.headers['WWW-Authenticate']).to.eql('Basic realm="Authorization Required"'); + }); + + it('persists WWW-Authenticate header', async () => { + set(error, 'body.error.header[WWW-Authenticate]', 'Basic realm="Test"'); + set(client, 'mock.401', sinon.stub().returns(Promise.reject(error))); + await cluster.callWithRequest({}, 'mock.401', {}, { wrap401Errors: true }).catch(handler); + + sinon.assert.calledOnce(handler); + expect(handler.getCall(0).args[0].output.headers['WWW-Authenticate']).to.eql('Basic realm="Test"'); + }); + }); }); }); }); diff --git a/src/core_plugins/elasticsearch/lib/cluster.js b/src/core_plugins/elasticsearch/lib/cluster.js index 51a1c2c46fcbbf..119e3f1ae4d460 100644 --- a/src/core_plugins/elasticsearch/lib/cluster.js +++ b/src/core_plugins/elasticsearch/lib/cluster.js @@ -1,6 +1,7 @@ import elasticsearch from 'elasticsearch'; import { get, set, isEmpty, cloneDeep, pick } from 'lodash'; import toPath from 'lodash/internal/toPath'; +import Boom from 'boom'; import filterHeaders from './filter_headers'; import { parseConfig } from './parse_config'; @@ -19,17 +20,17 @@ export class Cluster { return this; } - callWithRequest = (req = {}, endpoint, clientParams = {}) => { + callWithRequest = (req = {}, endpoint, clientParams = {}, options = {}) => { if (req.headers) { const filteredHeaders = filterHeaders(req.headers, this.getRequestHeadersWhitelist()); set(clientParams, 'headers', filteredHeaders); } - return callAPI(this._noAuthClient, endpoint, clientParams); + return callAPI(this._noAuthClient, endpoint, clientParams, options); } - callWithInternalUser = (endpoint, clientParams = {}) => { - return callAPI(this._client, endpoint, clientParams); + callWithInternalUser = (endpoint, clientParams = {}, options = {}) => { + return callAPI(this._client, endpoint, clientParams, options); } getRequestHeadersWhitelist = () => getClonedProperty(this._config, 'requestHeadersWhitelist'); @@ -80,7 +81,8 @@ export class Cluster { } } -function callAPI(client, endpoint, clientParams = {}) { +function callAPI(client, endpoint, clientParams = {}, options = {}) { + const wrap401Errors = options.wrap401Errors !== false; const clientPath = toPath(endpoint); const api = get(client, clientPath); @@ -93,7 +95,17 @@ function callAPI(client, endpoint, clientParams = {}) { throw new Error(`called with an invalid endpoint: ${endpoint}`); } - return api.call(apiContext, clientParams); + return api.call(apiContext, clientParams).catch((err) => { + if (!wrap401Errors || err.statusCode !== 401) { + return Promise.reject(err); + } + + const boomError = Boom.boomify(err, { statusCode: err.statusCode }); + const wwwAuthHeader = get(err, 'body.error.header[WWW-Authenticate]'); + boomError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"'; + + throw boomError; + }); } function getClonedProperties(config, paths) {