[server/uiSettings+shortUrl] surface errors from es (#9214)

* [server/uiSettings+shortUrl] surface errors from es

* [uiExports/replaceInjectedVars] update the uiSettings stub

* [uiSettings] correct test cases after moving from 401 -> 403
This commit is contained in:
Spencer 2016-11-23 16:51:12 -07:00 committed by GitHub
parent 8d003a1675
commit 6f1cd39f65
18 changed files with 362 additions and 254 deletions

View file

@ -1,27 +0,0 @@
import getBasicAuthRealm from '../get_basic_auth_realm';
import expect from 'expect.js';
const exception = '[security_exception] missing authentication token for REST request [/logstash-*/_search],' +
' with: {"header":{"WWW-Authenticate":"Basic realm=\\"shield\\""}}';
describe('plugins/elasticsearch', function () {
describe('lib/get_basic_auth_realm', function () {
it('should return null if passed something other than a string', function () {
expect(getBasicAuthRealm({})).to.be(null);
expect(getBasicAuthRealm(500)).to.be(null);
expect(getBasicAuthRealm([exception])).to.be(null);
});
// TODO: This should be updated to match header strings when the client supports that
it('should return the realm when passed an elasticsearch security exception', function () {
expect(getBasicAuthRealm(exception)).to.be('shield');
});
it('should return null when no basic realm information is found', function () {
expect(getBasicAuthRealm('Basically nothing="the universe"')).to.be(null);
});
});
});

View file

@ -1,14 +1,14 @@
import _ from 'lodash';
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 = (server, client) => {
return (req, endpoint, params = {}) => {
return (req, endpoint, clientParams = {}, options = {}) => {
const wrap401Errors = options.wrap401Errors !== false;
const filteredHeaders = filterHeaders(req.headers, server.config().get('elasticsearch.requestHeadersWhitelist'));
_.set(params, 'headers', filteredHeaders);
_.set(clientParams, 'headers', filteredHeaders);
const path = toPath(endpoint);
const api = _.get(client, path);
let apiContext = _.get(client, path.slice(0, -1));
@ -16,16 +16,16 @@ module.exports = (server, client) => {
apiContext = client;
}
if (!api) throw new Error(`callWithRequest called with an invalid endpoint: ${endpoint}`);
return api.call(apiContext, params)
return api.call(apiContext, clientParams)
.catch((err) => {
if (err.status === 401) {
// TODO: The err.message is temporary until we have support for getting headers in the client.
// Once we have that, we should be able to pass the contents of the WWW-Authenticate head to getRealm
const realm = getBasicAuthRealm(err.message) || 'Authorization Required';
const options = { realm: realm };
return Promise.reject(Boom.unauthorized('Unauthorized', 'Basic', options));
if (!wrap401Errors || err.statusCode !== 401) {
return Promise.reject(err);
}
return Promise.reject(err);
const boomError = Boom.wrap(err, err.statusCode);
const wwwAuthHeader = _.get(err, 'body.error.header[WWW-Authenticate]');
boomError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"';
throw boomError;
});
};
};

View file

@ -1,7 +0,0 @@
export default function getBasicAuthRealm(message) {
if (!message || typeof message !== 'string') return null;
const parts = message.match(/Basic\ realm=\\"(.*)\\"/);
if (parts && parts.length === 2) return parts[1];
else return null;
};

View file

@ -8,12 +8,12 @@ export default function registerDelete(server) {
const { key } = req.params;
const uiSettings = server.uiSettings();
uiSettings
.remove(key)
.remove(req, key)
.then(() => uiSettings
.getUserProvided()
.getUserProvided(req)
.then(settings => reply({ settings }).type('application/json'))
)
.catch(reason => reply(Boom.wrap(reason)));
.catch(err => reply(Boom.wrap(err, err.statusCode)));
}
});
}

View file

@ -7,9 +7,9 @@ export default function registerGet(server) {
handler: function (req, reply) {
server
.uiSettings()
.getUserProvided()
.getUserProvided(req)
.then(settings => reply({ settings }).type('application/json'))
.catch(reason => reply(Boom.wrap(reason)));
.catch(err => reply(Boom.wrap(err, err.statusCode)));
}
});
}

View file

@ -9,12 +9,12 @@ export default function registerSet(server) {
const { value } = req.payload;
const uiSettings = server.uiSettings();
uiSettings
.set(key, value)
.set(req, key, value)
.then(() => uiSettings
.getUserProvided()
.getUserProvided(req)
.then(settings => reply({ settings }).type('application/json'))
)
.catch(reason => reply(Boom.wrap(reason)));
.catch(err => reply(Boom.wrap(err, err.statusCode)));
}
});
}

View file

@ -9,12 +9,12 @@ export default function registerSet(server) {
const { changes } = req.payload;
const uiSettings = server.uiSettings();
uiSettings
.setMany(changes)
.setMany(req, changes)
.then(() => uiSettings
.getUserProvided()
.getUserProvided(req)
.then(settings => reply({ settings }).type('application/json'))
)
.catch(reason => reply(Boom.wrap(reason)));
.catch(err => reply(Boom.wrap(err, err.statusCode)));
}
});
}

View file

@ -10,56 +10,43 @@ function replyWithError(e, reply) {
module.exports = (server) => {
server.route({
method: ['POST', 'GET'],
path: '/api/timelion/run',
handler: (request, reply) => {
handler: async (request, reply) => {
try {
const uiSettings = await server.uiSettings().getAll(request);
// I don't really like this, but we need to get all of the settings
// before every request. This just sucks because its going to slow things
// down. Meh.
return server.uiSettings().getAll().then((uiSettings) => {
var sheet;
var tlConfig = require('../handlers/lib/tl_config.js')({
server: server,
request: request,
const tlConfig = require('../handlers/lib/tl_config.js')({
server,
request,
settings: _.defaults(uiSettings, timelionDefaults) // Just in case they delete some setting.
});
var chainRunner = chainRunnerFn(tlConfig);
try {
sheet = chainRunner.processRequest(request.payload || {
sheet: [request.query.expression],
time: {
from: request.query.from,
to: request.query.to,
interval: request.query.interval,
timezone: request.query.timezone
}
});
} catch (e) {
replyWithError(e, reply);
return;
}
return Promise.all(sheet).then((sheet) => {
var response = {
sheet: sheet,
stats: chainRunner.getStats()
};
reply(response);
}).catch((e) => {
// TODO Maybe we should just replace everywhere we throw with Boom? Probably.
if (e.isBoom) {
reply(e);
} else {
replyWithError(e, reply);
const chainRunner = chainRunnerFn(tlConfig);
const sheet = await Promise.all(chainRunner.processRequest(request.payload || {
sheet: [request.query.expression],
time: {
from: request.query.from,
to: request.query.to,
interval: request.query.interval,
timezone: request.query.timezone
}
}));
reply({
sheet,
stats: chainRunner.getStats()
});
});
} catch (err) {
// TODO Maybe we should just replace everywhere we throw with Boom? Probably.
if (err.isBoom) {
reply(err);
} else {
replyWithError(err, reply);
}
}
}
});

View file

@ -4,7 +4,7 @@ module.exports = function (server) {
path: '/api/timelion/validate/es',
handler: function (request, reply) {
return server.uiSettings().getAll().then((uiSettings) => {
return server.uiSettings().getAll(request).then((uiSettings) => {
var callWithRequest = server.plugins.elasticsearch.callWithRequest;
var timefield = uiSettings['timelion:es.timefield'];

View file

@ -0,0 +1,32 @@
import Boom from 'boom';
import expect from 'expect.js';
import _ from 'lodash';
import { handleShortUrlError } from '../short_url_error';
describe('handleShortUrlError()', () => {
const caughtErrors = [{
status: 401
}, {
status: 403
}, {
status: 404
}];
const uncaughtErrors = [{
status: null
}, {
status: 500
}];
caughtErrors.forEach((err) => {
it(`should handle ${err.status} errors`, function () {
expect(_.get(handleShortUrlError(err), 'output.statusCode')).to.be(err.status);
});
});
uncaughtErrors.forEach((err) => {
it(`should not handle unknown errors`, function () {
expect(_.get(handleShortUrlError(err), 'output.statusCode')).to.be(500);
});
});
});

View file

@ -6,6 +6,7 @@ import Boom from 'boom';
import Hapi from 'hapi';
import getDefaultRoute from './get_default_route';
import versionCheckMixin from './version_check';
import { handleShortUrlError } from './short_url_error';
import { shortUrlAssertValid } from './short_url_assert_valid';
module.exports = async function (kbnServer, server, config) {
@ -114,11 +115,11 @@ module.exports = async function (kbnServer, server, config) {
path: '/goto/{urlId}',
handler: async function (request, reply) {
try {
const url = await shortUrlLookup.getUrl(request.params.urlId);
const url = await shortUrlLookup.getUrl(request.params.urlId, request);
shortUrlAssertValid(url);
reply().redirect(config.get('server.basePath') + url);
} catch (err) {
reply(err);
reply(handleShortUrlError(err));
}
}
});
@ -129,10 +130,10 @@ module.exports = async function (kbnServer, server, config) {
handler: async function (request, reply) {
try {
shortUrlAssertValid(request.payload.url);
const urlId = await shortUrlLookup.generateUrlId(request.payload.url);
const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request);
reply(urlId);
} catch (err) {
reply(err);
reply(handleShortUrlError(err));
}
}
});

View file

@ -0,0 +1,9 @@
import Boom from 'boom';
export function handleShortUrlError(err) {
if (err.isBoom) return err;
if (err.status === 401) return Boom.unauthorized();
if (err.status === 403) return Boom.forbidden();
if (err.status === 404) return Boom.notFound();
return Boom.badImplementation();
}

View file

@ -1,12 +1,12 @@
import crypto from 'crypto';
export default function (server) {
async function updateMetadata(urlId, urlDoc) {
const client = server.plugins.elasticsearch.client;
async function updateMetadata(urlId, urlDoc, req) {
const callWithRequest = server.plugins.elasticsearch.callWithRequest;
const kibanaIndex = server.config().get('kibana.index');
try {
await client.update({
await callWithRequest(req, 'update', {
index: kibanaIndex,
type: 'url',
id: urlId,
@ -23,12 +23,12 @@ export default function (server) {
}
}
async function getUrlDoc(urlId) {
async function getUrlDoc(urlId, req) {
const urlDoc = await new Promise((resolve, reject) => {
const client = server.plugins.elasticsearch.client;
const callWithRequest = server.plugins.elasticsearch.callWithRequest;
const kibanaIndex = server.config().get('kibana.index');
client.get({
callWithRequest(req, 'get', {
index: kibanaIndex,
type: 'url',
id: urlId
@ -44,12 +44,12 @@ export default function (server) {
return urlDoc;
}
async function createUrlDoc(url, urlId) {
async function createUrlDoc(url, urlId, req) {
const newUrlId = await new Promise((resolve, reject) => {
const client = server.plugins.elasticsearch.client;
const callWithRequest = server.plugins.elasticsearch.callWithRequest;
const kibanaIndex = server.config().get('kibana.index');
client.index({
callWithRequest(req, 'index', {
index: kibanaIndex,
type: 'url',
id: urlId,
@ -80,19 +80,19 @@ export default function (server) {
}
return {
async generateUrlId(url) {
async generateUrlId(url, req) {
const urlId = createUrlId(url);
const urlDoc = await getUrlDoc(urlId);
const urlDoc = await getUrlDoc(urlId, req);
if (urlDoc) return urlId;
return createUrlDoc(url, urlId);
return createUrlDoc(url, urlId, req);
},
async getUrl(urlId) {
async getUrl(urlId, req) {
try {
const urlDoc = await getUrlDoc(urlId);
const urlDoc = await getUrlDoc(urlId, req);
if (!urlDoc) throw new Error('Requested shortened url does not exist in kibana index');
updateMetadata(urlId, urlDoc);
updateMetadata(urlId, urlDoc, req);
return urlDoc._source.url;
} catch (err) {

View file

@ -41,7 +41,7 @@ describe('UiExports', function () {
await kbnServer.ready();
kbnServer.status.get('ui settings').state = 'green';
kbnServer.server.decorate('server', 'uiSettings', () => {
return { getDefaults: noop };
return { getDefaults: noop, getUserProvided: noop };
});
});

View file

@ -1,6 +1,7 @@
import { format as formatUrl } from 'url';
import { readFileSync as readFile } from 'fs';
import { defaults } from 'lodash';
import { props } from 'bluebird';
import Boom from 'boom';
import { reduce as reduceAsync } from 'bluebird';
import { resolve } from 'path';
@ -61,7 +62,9 @@ export default async (kbnServer, server, config) => {
}
});
async function getPayload(app) {
async function getKibanaPayload({ app, request, includeUserProvidedConfig }) {
const uiSettings = server.uiSettings();
return {
app: app,
nav: uiExports.navLinks.inOrder,
@ -71,42 +74,47 @@ export default async (kbnServer, server, config) => {
basePath: config.get('server.basePath'),
serverName: config.get('server.name'),
devMode: config.get('env.dev'),
uiSettings: {
defaults: await server.uiSettings().getDefaults(),
user: {}
},
uiSettings: await props({
defaults: uiSettings.getDefaults(),
user: includeUserProvidedConfig && uiSettings.getUserProvided(request)
}),
vars: await reduceAsync(
uiExports.injectedVarsReplacers,
async (acc, replacer) => await replacer(acc, this.request, server),
async (acc, replacer) => await replacer(acc, request, server),
defaults(await app.getInjectedVars() || {}, uiExports.defaultInjectedVars)
)
),
};
}
function viewAppWithPayload(app, payload) {
return this.view(app.templateName, {
app: app,
kibanaPayload: payload,
bundlePath: `${config.get('server.basePath')}/bundles`,
});
}
async function renderApp(app) {
const payload = await getPayload.call(this, app);
const esStatus = kbnServer.status.getForPluginId('elasticsearch');
if (esStatus && esStatus.state !== 'red') {
payload.uiSettings.user = await server.uiSettings().getUserProvided();
async function renderApp({ app, reply, includeUserProvidedConfig = true }) {
try {
return reply.view(app.templateName, {
app,
kibanaPayload: await getKibanaPayload({
app,
request: reply.request,
includeUserProvidedConfig
}),
bundlePath: `${config.get('server.basePath')}/bundles`,
});
} catch (err) {
reply(err);
}
return viewAppWithPayload.call(this, app, payload);
}
async function renderAppWithDefaultConfig(app) {
const payload = await getPayload.call(this, app);
return viewAppWithPayload.call(this, app, payload);
}
server.decorate('reply', 'renderApp', function (app) {
return renderApp({
app,
reply: this,
includeUserProvidedConfig: true,
});
});
server.decorate('reply', 'renderApp', renderApp);
server.decorate('reply', 'renderAppWithDefaultConfig', renderAppWithDefaultConfig);
server.decorate('reply', 'renderAppWithDefaultConfig', function (app) {
return renderApp({
app,
reply: this,
includeUserProvidedConfig: false,
});
});
};

View file

@ -91,7 +91,12 @@ any custom setting configuration watchers for "${key}" may fix this issue.`);
if (value === null) {
delete settings[key].userValue;
} else {
settings[key].userValue = value;
const { type } = settings[key];
if (type === 'json' && typeof value !== 'string') {
settings[key].userValue = angular.toJson(value);
} else {
settings[key].userValue = value;
}
}
}

View file

@ -3,6 +3,7 @@ import sinon from 'sinon';
import expect from 'expect.js';
import init from '..';
import defaultsProvider from '../defaults';
import { errors as esErrors } from 'elasticsearch';
describe('ui settings', function () {
describe('overview', function () {
@ -22,23 +23,23 @@ describe('ui settings', function () {
describe('#setMany()', function () {
it('returns a promise', () => {
const { uiSettings } = instantiate();
const result = uiSettings.setMany({ a: 'b' });
const { uiSettings, req } = instantiate();
const result = uiSettings.setMany(req, { a: 'b' });
expect(result).to.be.a(Promise);
});
it('updates a single value in one operation', function () {
const { server, uiSettings, configGet } = instantiate();
const result = uiSettings.setMany({ one: 'value' });
expectElasticsearchUpdateQuery(server, configGet, {
const { server, uiSettings, configGet, req } = instantiate();
const result = uiSettings.setMany(req, { one: 'value' });
expectElasticsearchUpdateQuery(server, req, configGet, {
one: 'value'
});
});
it('updates several values in one operation', function () {
const { server, uiSettings, configGet } = instantiate();
const result = uiSettings.setMany({ one: 'value', another: 'val' });
expectElasticsearchUpdateQuery(server, configGet, {
const { server, uiSettings, configGet, req } = instantiate();
const result = uiSettings.setMany(req, { one: 'value', another: 'val' });
expectElasticsearchUpdateQuery(server, req, configGet, {
one: 'value', another: 'val'
});
});
@ -46,15 +47,15 @@ describe('ui settings', function () {
describe('#set()', function () {
it('returns a promise', () => {
const { uiSettings } = instantiate();
const result = uiSettings.set('a', 'b');
const { uiSettings, req } = instantiate();
const result = uiSettings.set(req, 'a', 'b');
expect(result).to.be.a(Promise);
});
it('updates single values by (key, value)', function () {
const { server, uiSettings, configGet } = instantiate();
const result = uiSettings.set('one', 'value');
expectElasticsearchUpdateQuery(server, configGet, {
const { server, uiSettings, configGet, req } = instantiate();
const result = uiSettings.set(req, 'one', 'value');
expectElasticsearchUpdateQuery(server, req, configGet, {
one: 'value'
});
});
@ -62,15 +63,15 @@ describe('ui settings', function () {
describe('#remove()', function () {
it('returns a promise', () => {
const { uiSettings } = instantiate();
const result = uiSettings.remove('one');
const { uiSettings, req } = instantiate();
const result = uiSettings.remove(req, 'one');
expect(result).to.be.a(Promise);
});
it('removes single values by key', function () {
const { server, uiSettings, configGet } = instantiate();
const result = uiSettings.remove('one');
expectElasticsearchUpdateQuery(server, configGet, {
const { server, uiSettings, configGet, req } = instantiate();
const result = uiSettings.remove(req, 'one');
expectElasticsearchUpdateQuery(server, req, configGet, {
one: null
});
});
@ -78,23 +79,23 @@ describe('ui settings', function () {
describe('#removeMany()', function () {
it('returns a promise', () => {
const { uiSettings } = instantiate();
const result = uiSettings.removeMany(['one']);
const { uiSettings, req } = instantiate();
const result = uiSettings.removeMany(req, ['one']);
expect(result).to.be.a(Promise);
});
it('removes a single value', function () {
const { server, uiSettings, configGet } = instantiate();
const result = uiSettings.removeMany(['one']);
expectElasticsearchUpdateQuery(server, configGet, {
const { server, uiSettings, configGet, req } = instantiate();
const result = uiSettings.removeMany(req, ['one']);
expectElasticsearchUpdateQuery(server, req, configGet, {
one: null
});
});
it('updates several values in one operation', function () {
const { server, uiSettings, configGet } = instantiate();
const result = uiSettings.removeMany(['one', 'two', 'three']);
expectElasticsearchUpdateQuery(server, configGet, {
const { server, uiSettings, configGet, req } = instantiate();
const result = uiSettings.removeMany(req, ['one', 'two', 'three']);
expectElasticsearchUpdateQuery(server, req, configGet, {
one: null, two: null, three: null
});
});
@ -134,15 +135,15 @@ describe('ui settings', function () {
describe('#getUserProvided()', function () {
it('pulls user configuration from ES', async function () {
const getResult = { user: 'customized' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getUserProvided();
expectElasticsearchGetQuery(server, configGet);
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getUserProvided(req);
expectElasticsearchGetQuery(server, req, configGet);
});
it('returns user configuration', async function () {
const getResult = { user: 'customized' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getUserProvided();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getUserProvided(req);
expect(isEqual(result, {
user: { userValue: 'customized' }
})).to.equal(true);
@ -150,33 +151,95 @@ describe('ui settings', function () {
it('ignores null user configuration (because default values)', async function () {
const getResult = { user: 'customized', usingDefault: null, something: 'else' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getUserProvided();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getUserProvided(req);
expect(isEqual(result, {
user: { userValue: 'customized' }, something: { userValue: 'else' }
})).to.equal(true);
});
it('returns an empty object on 404 responses', async function () {
const { uiSettings, req } = instantiate({
async callWithRequest() {
throw new esErrors[404]();
}
});
expect(await uiSettings.getUserProvided(req)).to.eql({});
});
it('returns an empty object on 403 responses', async function () {
const { uiSettings, req } = instantiate({
async callWithRequest() {
throw new esErrors[403]();
}
});
expect(await uiSettings.getUserProvided(req)).to.eql({});
});
it('returns an empty object on NoConnections responses', async function () {
const { uiSettings, req } = instantiate({
async callWithRequest() {
throw new esErrors.NoConnections();
}
});
expect(await uiSettings.getUserProvided(req)).to.eql({});
});
it('throws 401 errors', async function () {
const { uiSettings, req } = instantiate({
async callWithRequest() {
throw new esErrors[401]();
}
});
try {
await uiSettings.getUserProvided(req);
throw new Error('expect getUserProvided() to throw');
} catch (err) {
expect(err).to.be.a(esErrors[401]);
}
});
it('throw when callWithRequest fails in some unexpected way', async function () {
const expectedUnexpectedError = new Error('unexpected');
const { uiSettings, req } = instantiate({
async callWithRequest() {
throw expectedUnexpectedError;
}
});
try {
await uiSettings.getUserProvided(req);
throw new Error('expect getUserProvided() to throw');
} catch (err) {
expect(err).to.be(expectedUnexpectedError);
}
});
});
describe('#getRaw()', function () {
it('pulls user configuration from ES', async function () {
const getResult = {};
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getRaw();
expectElasticsearchGetQuery(server, configGet);
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getRaw(req);
expectElasticsearchGetQuery(server, req, configGet);
});
it(`without user configuration it's equal to the defaults`, async function () {
const getResult = {};
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getRaw();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getRaw(req);
expect(isEqual(result, defaultsProvider())).to.equal(true);
});
it(`user configuration gets merged with defaults`, async function () {
const getResult = { foo: 'bar' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getRaw();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getRaw(req);
const merged = defaultsProvider();
merged.foo = { userValue: 'bar' };
expect(isEqual(result, merged)).to.equal(true);
@ -184,8 +247,8 @@ describe('ui settings', function () {
it(`user configuration gets merged into defaults`, async function () {
const getResult = { dateFormat: 'YYYY-MM-DD' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getRaw();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getRaw(req);
const merged = defaultsProvider();
merged.dateFormat.userValue = 'YYYY-MM-DD';
expect(isEqual(result, merged)).to.equal(true);
@ -195,15 +258,15 @@ describe('ui settings', function () {
describe('#getAll()', function () {
it('pulls user configuration from ES', async function () {
const getResult = {};
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getAll();
expectElasticsearchGetQuery(server, configGet);
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getAll(req);
expectElasticsearchGetQuery(server, req, configGet);
});
it(`returns key value pairs`, async function () {
const getResult = {};
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getAll();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getAll(req);
const defaults = defaultsProvider();
const expectation = {};
Object.keys(defaults).forEach(key => {
@ -214,8 +277,8 @@ describe('ui settings', function () {
it(`returns key value pairs including user configuration`, async function () {
const getResult = { something: 'user-provided' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getAll();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getAll(req);
const defaults = defaultsProvider();
const expectation = {};
Object.keys(defaults).forEach(key => {
@ -227,8 +290,8 @@ describe('ui settings', function () {
it(`returns key value pairs including user configuration for existing settings`, async function () {
const getResult = { dateFormat: 'YYYY-MM-DD' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.getAll();
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.getAll(req);
const defaults = defaultsProvider();
const expectation = {};
Object.keys(defaults).forEach(key => {
@ -242,55 +305,63 @@ describe('ui settings', function () {
describe('#get()', function () {
it('pulls user configuration from ES', async function () {
const getResult = {};
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.get();
expectElasticsearchGetQuery(server, configGet);
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.get(req);
expectElasticsearchGetQuery(server, req, configGet);
});
it(`returns the promised value for a key`, async function () {
const getResult = {};
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.get('dateFormat');
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.get(req, 'dateFormat');
const defaults = defaultsProvider();
expect(result).to.equal(defaults.dateFormat.value);
});
it(`returns the user-configured value for a custom key`, async function () {
const getResult = { custom: 'value' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.get('custom');
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.get(req, 'custom');
expect(result).to.equal('value');
});
it(`returns the user-configured value for a modified key`, async function () {
const getResult = { dateFormat: 'YYYY-MM-DD' };
const { server, uiSettings, configGet } = instantiate({ getResult });
const result = await uiSettings.get('dateFormat');
const { server, uiSettings, configGet, req } = instantiate({ getResult });
const result = await uiSettings.get(req, 'dateFormat');
expect(result).to.equal('YYYY-MM-DD');
});
});
});
function expectElasticsearchGetQuery(server, configGet) {
expect(server.plugins.elasticsearch.client.get.callCount).to.equal(1);
expect(isEqual(server.plugins.elasticsearch.client.get.firstCall.args, [{
function expectElasticsearchGetQuery(server, req, configGet) {
const { callWithRequest } = server.plugins.elasticsearch;
sinon.assert.calledOnce(callWithRequest);
const [reqPassed, method, params] = callWithRequest.args[0];
expect(reqPassed).to.be(req);
expect(method).to.be('get');
expect(params).to.eql({
index: configGet('kibana.index'),
id: configGet('pkg.version'),
type: 'config'
}])).to.equal(true);
});
}
function expectElasticsearchUpdateQuery(server, configGet, doc) {
expect(server.plugins.elasticsearch.client.update.callCount).to.equal(1);
expect(isEqual(server.plugins.elasticsearch.client.update.firstCall.args, [{
function expectElasticsearchUpdateQuery(server, req, configGet, doc) {
const { callWithRequest } = server.plugins.elasticsearch;
sinon.assert.calledOnce(callWithRequest);
const [reqPassed, method, params] = callWithRequest.args[0];
expect(reqPassed).to.be(req);
expect(method).to.be('update');
expect(params).to.eql({
index: configGet('kibana.index'),
id: configGet('pkg.version'),
type: 'config',
body: { doc }
}])).to.equal(true);
});
}
function instantiate({ getResult } = {}) {
function instantiate({ getResult, callWithRequest } = {}) {
const esStatus = {
state: 'green',
on: sinon.spy()
@ -308,14 +379,27 @@ function instantiate({ getResult } = {}) {
},
ready: sinon.stub().returns(Promise.resolve())
};
const req = { __stubHapiRequest: true, path: '', headers: {} };
const server = {
decorate: (_, key, value) => server[key] = value,
plugins: {
elasticsearch: {
client: {
get: sinon.stub().returns(Promise.resolve({ _source: getResult })),
update: sinon.stub().returns(Promise.resolve())
}
errors: esErrors,
callWithRequest: sinon.spy((withReq, method, params) => {
if (callWithRequest) {
return callWithRequest(withReq, method, params);
}
expect(withReq).to.be(req);
switch (method) {
case 'get':
return Promise.resolve({ _source: getResult });
case 'update':
return Promise.resolve();
default:
throw new Error(`callWithRequest() is using unexpected method "${method}"`);
}
})
}
}
};
@ -328,5 +412,5 @@ function instantiate({ getResult } = {}) {
};
const setupSettings = init(kbnServer, server, config);
const uiSettings = server.uiSettings();
return { server, uiSettings, configGet };
return { server, uiSettings, configGet, req };
}

View file

@ -1,5 +1,6 @@
import { defaultsDeep, partial } from 'lodash';
import defaultsProvider from './defaults';
import Bluebird from 'bluebird';
export default function setupSettings(kbnServer, server, config) {
const status = kbnServer.status.create('ui settings');
@ -34,12 +35,14 @@ export default function setupSettings(kbnServer, server, config) {
server.decorate('server', 'uiSettings', () => uiSettings);
kbnServer.ready().then(mirrorEsStatus);
function get(key) {
return getAll().then(all => all[key]);
async function get(req, key) {
assertRequest(req);
return getAll(req).then(all => all[key]);
}
function getAll() {
return getRaw()
async function getAll(req) {
assertRequest(req);
return getRaw(req)
.then(raw => Object.keys(raw)
.reduce((all, key) => {
const item = raw[key];
@ -50,9 +53,10 @@ export default function setupSettings(kbnServer, server, config) {
);
}
function getRaw() {
async function getRaw(req) {
assertRequest(req);
return Promise
.all([getDefaults(), getUserProvided()])
.all([getDefaults(), getUserProvided(req)])
.then(([defaults, user]) => defaultsDeep(user, defaults));
}
@ -60,46 +64,48 @@ export default function setupSettings(kbnServer, server, config) {
return Promise.resolve(defaultsProvider());
}
function userSettingsNotFound(kibanaVersion) {
status.red(`Could not find user-provided settings for Kibana ${kibanaVersion}`);
return {};
async function getUserProvided(req, { ignore401Errors = false } = {}) {
assertRequest(req);
const { callWithRequest, errors } = server.plugins.elasticsearch;
const params = getClientSettings(config);
const allowedErrors = [errors[404], errors[403], errors.NoConnections];
if (ignore401Errors) allowedErrors.push(errors[401]);
return Bluebird.resolve(callWithRequest(req, 'get', params, { wrap401Errors: !ignore401Errors }))
.catch(...allowedErrors, err => ({}))
.then(resp => resp._source || {})
.then(source => hydrateUserSettings(source));
}
function getUserProvided() {
const { client } = server.plugins.elasticsearch;
const clientSettings = getClientSettings(config);
return client
.get({ ...clientSettings })
.then(res => res._source)
.catch(partial(userSettingsNotFound, clientSettings.id))
.then(user => hydrateUserSettings(user));
}
function setMany(changes) {
const { client } = server.plugins.elasticsearch;
const clientSettings = getClientSettings(config);
return client
.update({
...clientSettings,
body: { doc: changes }
})
async function setMany(req, changes) {
assertRequest(req);
const { callWithRequest } = server.plugins.elasticsearch;
const clientParams = {
...getClientSettings(config),
body: { doc: changes }
};
return callWithRequest(req, 'update', clientParams)
.then(() => ({}));
}
function set(key, value) {
return setMany({ [key]: value });
async function set(req, key, value) {
assertRequest(req);
return setMany(req, { [key]: value });
}
function remove(key) {
return set(key, null);
async function remove(req, key) {
assertRequest(req);
return set(req, key, null);
}
function removeMany(keys) {
async function removeMany(req, keys) {
assertRequest(req);
const changes = {};
keys.forEach(key => {
changes[key] = null;
});
return setMany(changes);
return setMany(req, changes);
}
function mirrorEsStatus() {
@ -138,3 +144,13 @@ function getClientSettings(config) {
const type = 'config';
return { index, type, id };
}
function assertRequest(req) {
if (
typeof req === 'object' &&
typeof req.path === 'string' &&
typeof req.headers === 'object'
) return;
throw new TypeError('all uiSettings methods must be passed a hapi.Request object');
}