diff --git a/config/kibana.yml b/config/kibana.yml index 62077469d4cd862..7f03500ae2742e7 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -39,22 +39,23 @@ #elasticsearch.username: "user" #elasticsearch.password: "pass" -# Paths to the PEM-format SSL certificate and SSL key files, respectively. These -# files enable SSL for outgoing requests from the Kibana server to the browser. -#server.ssl.cert: /path/to/your/server.crt +# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. +# These settings enable SSL for outgoing requests from the Kibana server to the browser. +#server.ssl.enabled: false +#server.ssl.certificate: /path/to/your/server.crt #server.ssl.key: /path/to/your/server.key # Optional settings that provide the paths to the PEM-format SSL certificate and key files. # These files validate that your Elasticsearch backend uses the same key files. -#elasticsearch.ssl.cert: /path/to/your/client.crt +#elasticsearch.ssl.certificate: /path/to/your/client.crt #elasticsearch.ssl.key: /path/to/your/client.key # Optional setting that enables you to specify a path to the PEM file for the certificate # authority for your Elasticsearch instance. -#elasticsearch.ssl.ca: /path/to/your/CA.pem +#elasticsearch.ssl.certificateAuthorities: /path/to/your/CA.pem -# To disregard the validity of SSL certificates, change this setting's value to false. -#elasticsearch.ssl.verify: true +# To disregard the validity of SSL certificates, change this setting's value to 'none'. +#elasticsearch.ssl.verificationMode: full # Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of # the elasticsearch.requestTimeout setting. diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index 7fa3ab08a491395..b9704c60a5886e0 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -43,14 +43,15 @@ to work with X-Pack, see {xpack-ref}kibana.html. Kibana supports SSL encryption for both client requests and the requests the Kibana server sends to Elasticsearch. -To encrypt communications between the browser and the Kibana server, you configure the `ssl_key_file` and -`ssl_cert_file` properties in `kibana.yml`: +To encrypt communications between the browser and the Kibana server, you configure the `server.ssl.enabled`, +`server.ssl.certificate` and `server.ssl.key` properties in `kibana.yml`: [source,text] ---- # SSL for outgoing requests from the Kibana Server (PEM formatted) +server.ssl.enabled: true server.ssl.key: /path/to/your/server.key -server.ssl.cert: /path/to/your/server.crt +server.ssl.certificate: /path/to/your/server.crt ---- If you are using X-Pack Security or a proxy that provides an HTTPS endpoint for Elasticsearch, @@ -62,17 +63,18 @@ protocol when you configure the Elasticsearch URL in `kibana.yml`: [source,text] ---- -elasticsearch: "https://.com:9200" +elasticsearch.url: "https://.com:9200" ---- -If you are using a self-signed certificate for Elasticsearch, set the `ca` property in -`kibana.yml` to specify the location of the PEM file. Setting the `ca` property lets you leave the `verify_ssl` option enabled. +If you are using a self-signed certificate for Elasticsearch, set the `certificateAuthorities` property in +`kibana.yml` to specify the location of the PEM file. Setting the `certificateAuthorities` property lets you use the +default `verificationMode` option of `full`. [source,text] ---- # If you need to provide a CA certificate for your Elasticsearch instance, put # the path of the pem file here. -ca: /path/to/your/ca/cacert.pem +elasticsearch.ssl.certificateAuthorities: /path/to/your/ca/cacert.pem ---- [float] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index bf31ba692dec95d..92a071a9626209c 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -33,14 +33,20 @@ Specify the position of the subdomain the URL with the token `{s}`. `elasticsearch.username:` and `elasticsearch.password:`:: If your Elasticsearch is protected with basic authentication, these settings provide the username and password that the Kibana server uses to perform maintenance on the Kibana index at startup. Your Kibana users still need to authenticate with Elasticsearch, which is proxied through the Kibana server. -`server.ssl.cert:` and `server.ssl.key:`:: Paths to the PEM-format SSL certificate and SSL key files, respectively. These -files enable SSL for outgoing requests from the Kibana server to the browser. +`server.ssl.enabled`:: *Default: "false"* Enables SSL for outgoing requests from the Kibana server to the browser. When set to `true`, `server.ssl.certificate` and `server.ssl.key` are required +`server.ssl.certificate:` and `server.ssl.key:`:: Paths to the PEM-format SSL certificate and SSL key files, respectively. +`server.ssl.keyPassphrase`:: The passphrase that will be used to decrypt the private key. This value is optional as the key may not be encrypted. +`server.ssl.certificateAuthorities`:: List of paths to PEM encoded certificate files that should be trusted. +`server.ssl.clientAuthentication`:: *Default: none* Controls Kibana's server behavior in regard to requesting a certificate from client connections. Valid values are `required`, +`optional`, and `none`. `required` forces a client to present a certificate, while `optional` requests a client certificate but the client is not required to present one. +`server.ssl.supportedProtocols`:: *Default: TLSv1, TLSv1.1, TLSv1.2* Supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2` `elasticsearch.ssl.cert:` and `elasticsearch.ssl.key:`:: Optional settings that provide the paths to the PEM-format SSL certificate and key files. These files validate that your Elasticsearch backend uses the same key files. -`elasticsearch.ssl.ca:`:: Optional setting that enables you to specify a path to the PEM file for the certificate +`elasticsearch.ssl.keyPassphrase`:: The passphrase that will be used to decrypt the private key. This value is optional as the key may not be encrypted. +`elasticsearch.ssl.certificateAuthorities:`:: Optional setting that enables you to specify a list of paths to the PEM file for the certificate authority for your Elasticsearch instance. -`elasticsearch.ssl.verify:`:: *Default: true* To disregard the validity of SSL certificates, change this setting’s value -to `false`. +`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the verification of certificates. Valid values are `none`, `certificate`, and `full`. +`full` performs hostname verification, and `certificate` does not. `elasticsearch.pingTimeout:`:: *Default: the value of the `elasticsearch.requestTimeout` setting* Time in milliseconds to wait for Elasticsearch to respond to pings. `elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or diff --git a/src/cli/cluster/base_path_proxy.js b/src/cli/cluster/base_path_proxy.js index 477154d26726328..bba62d3179d7baa 100644 --- a/src/cli/cluster/base_path_proxy.js +++ b/src/cli/cluster/base_path_proxy.js @@ -1,8 +1,8 @@ import { Server } from 'hapi'; import { notFound } from 'boom'; -import { merge, sample } from 'lodash'; +import { map, merge, sample } from 'lodash'; import { format as formatUrl } from 'url'; -import { map, fromNode } from 'bluebird'; +import { map as promiseMap, fromNode } from 'bluebird'; import { Agent as HttpsAgent } from 'https'; import { readFileSync } from 'fs'; @@ -10,6 +10,7 @@ import Config from '../../server/config/config'; import setupConnection from '../../server/http/setup_connection'; import registerHapiPlugins from '../../server/http/register_hapi_plugins'; import setupLogging from '../../server/logging'; +import { transformDeprecations } from '../../server/config/transform_deprecations'; import { DEV_SSL_CERT_PATH } from '../dev_ssl'; const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); @@ -19,20 +20,21 @@ export default class BasePathProxy { this.clusterManager = clusterManager; this.server = new Server(); - const config = Config.withDefaultSchema(userSettings); + const settings = transformDeprecations(userSettings); + const config = Config.withDefaultSchema(settings); this.targetPort = config.get('dev.basePathProxyTarget'); this.basePath = config.get('server.basePath'); - const { cert } = config.get('server.ssl'); - if (cert) { - const httpsAgentConfig = {}; - if (cert === DEV_SSL_CERT_PATH && config.get('server.host') !== 'localhost') { - httpsAgentConfig.rejectUnauthorized = false; - } else { - httpsAgentConfig.ca = readFileSync(cert); - } - this.proxyAgent = new HttpsAgent(httpsAgentConfig); + const sslEnabled = config.get('server.ssl.enabled'); + if (sslEnabled) { + this.proxyAgent = new HttpsAgent({ + key: readFileSync(config.get('server.ssl.key')), + passphrase: config.get('server.ssl.keyPassphrase'), + cert: readFileSync(config.get('server.ssl.certificate')), + ca: map(config.get('server.ssl.certificateAuthorities'), readFileSync), + rejectUnauthorized: false + }); } if (!this.basePath) { @@ -67,7 +69,7 @@ export default class BasePathProxy { config: { pre: [ (req, reply) => { - map(clusterManager.workers, worker => { + promiseMap(clusterManager.workers, worker => { if (worker.type === 'server' && !worker.listening && !worker.crashed) { return fromNode(cb => { const done = () => { diff --git a/src/cli/serve/__tests__/deprecated_config.js b/src/cli/serve/__tests__/deprecated_config.js deleted file mode 100644 index c132e814dcd12f5..000000000000000 --- a/src/cli/serve/__tests__/deprecated_config.js +++ /dev/null @@ -1,48 +0,0 @@ -import expect from 'expect.js'; -import { set } from 'lodash'; -import { checkForDeprecatedConfig } from '../deprecated_config'; -import sinon from 'auto-release-sinon'; - -describe('cli/serve/deprecated_config', function () { - it('passes original config through', function () { - const config = {}; - set(config, 'server.xsrf.token', 'xxtokenxx'); - const output = checkForDeprecatedConfig(config); - expect(output).to.be(config); - expect(output.server).to.be(config.server); - expect(output.server.xsrf).to.be(config.server.xsrf); - expect(output.server.xsrf.token).to.be(config.server.xsrf.token); - }); - - it('logs warnings about deprecated config values', function () { - const log = sinon.stub(); - const config = {}; - set(config, 'server.xsrf.token', 'xxtokenxx'); - checkForDeprecatedConfig(config, log); - sinon.assert.calledOnce(log); - expect(log.firstCall.args[0]).to.match(/server\.xsrf\.token.+deprecated/); - }); - - describe('does not support compound.keys', function () { - it('ignores fully compound keys', function () { - const log = sinon.stub(); - const config = { 'server.xsrf.token': 'xxtokenxx' }; - checkForDeprecatedConfig(config, log); - sinon.assert.notCalled(log); - }); - - it('ignores partially compound keys', function () { - const log = sinon.stub(); - const config = { server: { 'xsrf.token': 'xxtokenxx' } }; - checkForDeprecatedConfig(config, log); - sinon.assert.notCalled(log); - }); - - it('ignores partially compound keys', function () { - const log = sinon.stub(); - const config = { 'server.xsrf': { token: 'xxtokenxx' } }; - checkForDeprecatedConfig(config, log); - sinon.assert.notCalled(log); - }); - }); -}); diff --git a/src/cli/serve/__tests__/fixtures/deprecated.yml b/src/cli/serve/__tests__/fixtures/deprecated.yml deleted file mode 100644 index 748197e8957f646..000000000000000 --- a/src/cli/serve/__tests__/fixtures/deprecated.yml +++ /dev/null @@ -1 +0,0 @@ -server.xsrf.token: token diff --git a/src/cli/serve/__tests__/fixtures/legacy.yml b/src/cli/serve/__tests__/fixtures/legacy.yml deleted file mode 100644 index 080a80941646c13..000000000000000 --- a/src/cli/serve/__tests__/fixtures/legacy.yml +++ /dev/null @@ -1 +0,0 @@ -kibana_index: indexname diff --git a/src/cli/serve/__tests__/legacy_config.js b/src/cli/serve/__tests__/legacy_config.js deleted file mode 100644 index a380ae9e485c9c9..000000000000000 --- a/src/cli/serve/__tests__/legacy_config.js +++ /dev/null @@ -1,28 +0,0 @@ -import expect from 'expect.js'; -import { rewriteLegacyConfig } from '../legacy_config'; -import sinon from 'auto-release-sinon'; - -describe('cli/serve/legacy_config', function () { - it('returns a clone of the input', function () { - const file = {}; - const output = rewriteLegacyConfig(file); - expect(output).to.not.be(file); - }); - - it('rewrites legacy config values with literal path replacement', function () { - const file = { port: 4000, host: 'kibana.com' }; - const output = rewriteLegacyConfig(file); - expect(output).to.not.be(file); - expect(output).to.eql({ - 'server.port': 4000, - 'server.host': 'kibana.com', - }); - }); - - it('logs warnings when legacy config properties are encountered', function () { - const log = sinon.stub(); - rewriteLegacyConfig({ port: 5555 }, log); - sinon.assert.calledOnce(log); - expect(log.firstCall.args[0]).to.match(/port.+deprecated.+server\.port/); - }); -}); diff --git a/src/cli/serve/__tests__/read_yaml_config.js b/src/cli/serve/__tests__/read_yaml_config.js index 29b620b27dbb005..1358620f59b02a0 100644 --- a/src/cli/serve/__tests__/read_yaml_config.js +++ b/src/cli/serve/__tests__/read_yaml_config.js @@ -57,46 +57,4 @@ describe('cli/serve/read_yaml_config', function () { process.chdir(oldCwd); }); }); - - context('stubbed stdout', function () { - let stub; - - beforeEach(function () { - stub = sinon.stub(process.stdout, 'write'); - }); - - context('deprecated settings', function () { - it('warns about deprecated settings', function () { - readYamlConfig(fixture('deprecated.yml')); - sinon.assert.calledOnce(stub); - expect(stub.firstCall.args[0]).to.match(/deprecated/); - stub.restore(); - }); - - it('only warns once about deprecated settings', function () { - readYamlConfig(fixture('deprecated.yml')); - readYamlConfig(fixture('deprecated.yml')); - readYamlConfig(fixture('deprecated.yml')); - sinon.assert.notCalled(stub); // already logged in previous test - stub.restore(); - }); - }); - - context('legacy settings', function () { - it('warns about deprecated settings', function () { - readYamlConfig(fixture('legacy.yml')); - sinon.assert.calledOnce(stub); - expect(stub.firstCall.args[0]).to.match(/has been replaced/); - stub.restore(); - }); - - it('only warns once about legacy settings', function () { - readYamlConfig(fixture('legacy.yml')); - readYamlConfig(fixture('legacy.yml')); - readYamlConfig(fixture('legacy.yml')); - sinon.assert.notCalled(stub); // already logged in previous test - stub.restore(); - }); - }); - }); }); diff --git a/src/cli/serve/deprecated_config.js b/src/cli/serve/deprecated_config.js deleted file mode 100644 index d0ec271a8cee8c0..000000000000000 --- a/src/cli/serve/deprecated_config.js +++ /dev/null @@ -1,16 +0,0 @@ -import { forOwn, has, noop } from 'lodash'; - -// deprecated settings are still allowed, but will be removed at a later time. They -// are checked for after the config object is prepared and known, so legacySettings -// will have already been transformed. -export const deprecatedSettings = new Map([ - [['server', 'xsrf', 'token'], 'server.xsrf.token is deprecated. It is no longer used when providing xsrf protection.'] -]); - -// check for and warn about deprecated settings -export function checkForDeprecatedConfig(object, log = noop) { - for (const [key, msg] of deprecatedSettings.entries()) { - if (has(object, key)) log(msg); - } - return object; -} diff --git a/src/cli/serve/legacy_config.js b/src/cli/serve/legacy_config.js deleted file mode 100644 index 591a4fd1a6f8c0a..000000000000000 --- a/src/cli/serve/legacy_config.js +++ /dev/null @@ -1,52 +0,0 @@ -import { noop, transform } from 'lodash'; - -// legacySettings allow kibana 4.2+ to accept the same config file that people -// used for kibana 4.0 and 4.1. These settings are transformed to their modern -// equivalents at the very begining of the process -export const legacySettings = { - // server - port: 'server.port', - host: 'server.host', - pid_file: 'pid.file', - ssl_cert_file: 'server.ssl.cert', - ssl_key_file: 'server.ssl.key', - - // logging - log_file: 'logging.dest', - - // kibana - kibana_index: 'kibana.index', - default_app_id: 'kibana.defaultAppId', - - // es - ca: 'elasticsearch.ssl.ca', - elasticsearch_preserve_host: 'elasticsearch.preserveHost', - elasticsearch_url: 'elasticsearch.url', - kibana_elasticsearch_client_crt: 'elasticsearch.ssl.cert', - kibana_elasticsearch_client_key: 'elasticsearch.ssl.key', - kibana_elasticsearch_password: 'elasticsearch.password', - kibana_elasticsearch_username: 'elasticsearch.username', - ping_timeout: 'elasticsearch.pingTimeout', - request_timeout: 'elasticsearch.requestTimeout', - shard_timeout: 'elasticsearch.shardTimeout', - startup_timeout: 'elasticsearch.startupTimeout', - tilemap_url: 'tilemap.url', - tilemap_min_zoom: 'tilemap.options.minZoom', - tilemap_max_zoom: 'tilemap.options.maxZoom', - tilemap_attribution: 'tilemap.options.attribution', - tilemap_subdomains: 'tilemap.options.subdomains', - verify_ssl: 'elasticsearch.ssl.verify', -}; - -// transform legacy options into new namespaced versions -export function rewriteLegacyConfig(object, log = noop) { - return transform(object, (clone, val, key) => { - if (legacySettings.hasOwnProperty(key)) { - const replacement = legacySettings[key]; - log(`Config key "${key}" is deprecated. It has been replaced with "${replacement}"`); - clone[replacement] = val; - } else { - clone[key] = val; - } - }, {}); -} diff --git a/src/cli/serve/read_yaml_config.js b/src/cli/serve/read_yaml_config.js index 18e3a4520e87596..e929fde4f48a425 100644 --- a/src/cli/serve/read_yaml_config.js +++ b/src/cli/serve/read_yaml_config.js @@ -1,15 +1,9 @@ -import { chain, isArray, isPlainObject, forOwn, memoize, set, transform } from 'lodash'; +import { isArray, isPlainObject, forOwn, set, transform } from 'lodash'; import { readFileSync as read } from 'fs'; import { safeLoad } from 'js-yaml'; -import { red } from 'ansicolors'; -import { fromRoot } from '../../utils'; -import { rewriteLegacyConfig } from './legacy_config'; -import { checkForDeprecatedConfig } from './deprecated_config'; -const log = memoize(function (message) { - console.log(red('WARNING:'), message); -}); +import { fromRoot } from '../../utils'; export function merge(sources) { return transform(sources, (merged, source) => { @@ -35,6 +29,5 @@ export function merge(sources) { export default function (paths) { const files = [].concat(paths || []); const yamls = files.map(path => safeLoad(read(path, 'utf8'))); - const config = merge(yamls.map(file => rewriteLegacyConfig(file, log))); - return checkForDeprecatedConfig(config, log); + return merge(yamls); } diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 853553ad8f01e20..2fc9093a88080e5 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -38,8 +38,13 @@ function readServerSettings(opts, extraCliOptions) { if (opts.dev) { set('env', 'development'); set('optimize.lazy', true); - if (opts.ssl && !has('server.ssl.cert') && !has('server.ssl.key')) { - set('server.ssl.cert', DEV_SSL_CERT_PATH); + + if (opts.ssl) { + set('server.ssl.enabled', true); + } + + if (opts.ssl && !has('server.ssl.certificate') && !has('server.ssl.key')) { + set('server.ssl.certificate', DEV_SSL_CERT_PATH); set('server.ssl.key', DEV_SSL_KEY_PATH); } } diff --git a/src/core_plugins/console/__tests__/index.js b/src/core_plugins/console/__tests__/index.js new file mode 100644 index 000000000000000..7f85a31a6ee3251 --- /dev/null +++ b/src/core_plugins/console/__tests__/index.js @@ -0,0 +1,56 @@ +import { Deprecations } from '../../../deprecation'; +import expect from 'expect.js'; +import index from '../index'; +import { noop } from 'lodash'; +import sinon from 'sinon'; + +describe('plugins/console', function () { + describe('#deprecate()', function () { + let transformDeprecations; + + before(function () { + const Plugin = function (options) { + this.deprecations = options.deprecations; + }; + + const plugin = index({Plugin}); + + const deprecations = plugin.deprecations(Deprecations); + transformDeprecations = (settings, log = noop) => { + deprecations.forEach(deprecation => deprecation(settings, log)); + }; + }); + + context("proxyConfig", function () { + it('leaves the proxyConfig settings', function () { + const proxyConfigOne = {}; + const proxyConfigTwo = {}; + const settings = { + proxyConfig: [proxyConfigOne, proxyConfigTwo] + }; + + transformDeprecations(settings); + expect(settings.proxyConfig[0]).to.be(proxyConfigOne); + expect(settings.proxyConfig[1]).to.be(proxyConfigTwo); + }); + + it('logs a warning when proxyConfig is specified', function () { + const settings = { + proxyConfig: [] + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.calledOnce).to.be(true); + }); + + it(`doesn't log a warning when proxyConfig isn't specified`, function () { + const settings = {}; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + }); + }); +}); diff --git a/src/core_plugins/console/index.js b/src/core_plugins/console/index.js index eb61dc40f8e36a3..4b6b3e34314b7c8 100644 --- a/src/core_plugins/console/index.js +++ b/src/core_plugins/console/index.js @@ -1,4 +1,6 @@ +import { has } from 'lodash'; import { ProxyConfigCollection } from './server/proxy_config_collection'; +import { getElasticsearchProxyConfig } from './server/elasticsearch_proxy_config'; module.exports = function (kibana) { let { resolve, join, sep } = require('path'); @@ -49,22 +51,18 @@ module.exports = function (kibana) { key: Joi.string() }).default() }) - ).default([ - { - match: { - protocol: '*', - host: '*', - port: '*', - path: '*' - }, + ).default() + }).default(); + }, - timeout: 180000, - ssl: { - verify: true - } + deprecations: function () { + return [ + (settings, log) => { + if (has(settings, 'proxyConfig')) { + log('Config key "proxyConfig" is deprecated. Configuration can be inferred from the "elasticsearch" settings'); } - ]) - }).default(); + } + ]; }, init: function (server, options) { @@ -107,6 +105,14 @@ module.exports = function (kibana) { const requestHeadersWhitelist = server.config().get('elasticsearch.requestHeadersWhitelist'); const filterHeaders = server.plugins.elasticsearch.filterHeaders; + + let additionalConfig; + if (server.config().get('console.proxyConfig')) { + additionalConfig = proxyConfigCollection.configForUri(uri); + } else { + additionalConfig = getElasticsearchProxyConfig(server); + } + reply.proxy({ mapUri: function (request, done) { done(null, uri, filterHeaders(request.headers, requestHeadersWhitelist)) @@ -120,7 +126,7 @@ module.exports = function (kibana) { } }, - ...proxyConfigCollection.configForUri(uri) + ...additionalConfig }) } }; diff --git a/src/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js b/src/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js new file mode 100644 index 000000000000000..9c279bb867b0f48 --- /dev/null +++ b/src/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js @@ -0,0 +1,121 @@ +import expect from 'expect.js'; +import { getElasticsearchProxyConfig } from '../elasticsearch_proxy_config'; +import https from 'https'; +import http from 'http'; +import sinon from 'sinon'; + +describe('plugins/console', function () { + describe('#getElasticsearchProxyConfig', function () { + + let server; + + beforeEach(function () { + const stub = sinon.stub(); + server = { + config() { + return { + get: stub + }; + } + }; + + server.config().get.withArgs('elasticsearch.url').returns('http://localhost:9200'); + server.config().get.withArgs('elasticsearch.ssl.verificationMode').returns('full'); + }); + + const setElasticsearchConfig = (key, value) => { + server.config().get.withArgs(`elasticsearch.${key}`).returns(value); + }; + + it('sets timeout', function () { + const value = 1000; + setElasticsearchConfig('requestTimeout', value); + const proxyConfig = getElasticsearchProxyConfig(server); + expect(proxyConfig.timeout).to.be(value); + }); + + it(`uses https.Agent when url's protocol is https`, function () { + setElasticsearchConfig('url', 'https://localhost:9200'); + const { agent } = getElasticsearchProxyConfig(server); + expect(agent).to.be.a(https.Agent); + }); + + it(`uses http.Agent when url's protocol is http`, function () { + setElasticsearchConfig('url', 'http://localhost:9200'); + const { agent } = getElasticsearchProxyConfig(server); + expect(agent).to.be.a(http.Agent); + }); + + context('ssl', function () { + beforeEach(function () { + setElasticsearchConfig('url', 'https://localhost:9200'); + }); + + it('sets rejectUnauthorized to false when verificationMode is none', function () { + setElasticsearchConfig('ssl.verificationMode', 'none'); + const { agent } = getElasticsearchProxyConfig(server); + expect(agent.options.rejectUnauthorized).to.be(false); + }); + + it('sets rejectUnauthorized to true when verificationMode is certificate', function () { + setElasticsearchConfig('ssl.verificationMode', 'certificate'); + const { agent } = getElasticsearchProxyConfig(server); + expect(agent.options.rejectUnauthorized).to.be(true); + }); + + it('sets checkServerIdentity to not check hostname when verificationMode is certificate', function () { + setElasticsearchConfig('ssl.verificationMode', 'certificate'); + const { agent } = getElasticsearchProxyConfig(server); + + const cert = { + subject: { + CN: 'wrong.com' + } + }; + + expect(agent.options.checkServerIdentity).withArgs('right.com', cert).to.not.throwException(); + const result = agent.options.checkServerIdentity('right.com', cert); + expect(result).to.be(undefined); + }); + + it('sets rejectUnauthorized to true when verificationMode is full', function () { + setElasticsearchConfig('ssl.verificationMode', 'full'); + const { agent } = getElasticsearchProxyConfig(server); + + expect(agent.options.rejectUnauthorized).to.be(true); + }); + + it(`doesn't set checkServerIdentity when verificationMode is full`, function () { + setElasticsearchConfig('ssl.verificationMode', 'full'); + const { agent } = getElasticsearchProxyConfig(server); + + expect(agent.options.checkServerIdentity).to.be(undefined); + }); + + it(`sets ca when certificateAuthorities are specified`, function () { + setElasticsearchConfig('ssl.certificateAuthorities', [__dirname + '/fixtures/ca.crt']); + + const { agent } = getElasticsearchProxyConfig(server); + expect(agent.options.ca).to.contain('test ca certificate\n'); + }); + + it(`sets cert and key when certificate and key paths are specified`, function () { + setElasticsearchConfig('ssl.certificate', __dirname + '/fixtures/cert.crt'); + setElasticsearchConfig('ssl.key', __dirname + '/fixtures/cert.key'); + + const { agent } = getElasticsearchProxyConfig(server); + expect(agent.options.cert).to.be('test certificate\n'); + expect(agent.options.key).to.be('test key\n'); + }); + + it(`sets passphrase when certificate, key and keyPassphrase are specified`, function () { + setElasticsearchConfig('ssl.certificate', __dirname + '/fixtures/cert.crt'); + setElasticsearchConfig('ssl.key', __dirname + '/fixtures/cert.key'); + setElasticsearchConfig('ssl.keyPassphrase', 'secret'); + + const { agent } = getElasticsearchProxyConfig(server); + expect(agent.options.passphrase).to.be('secret'); + }); + }); + }); +}); diff --git a/src/core_plugins/console/server/__tests__/fixtures/ca.crt b/src/core_plugins/console/server/__tests__/fixtures/ca.crt new file mode 100644 index 000000000000000..075fdd038dafff8 --- /dev/null +++ b/src/core_plugins/console/server/__tests__/fixtures/ca.crt @@ -0,0 +1 @@ +test ca certificate diff --git a/src/core_plugins/console/server/__tests__/fixtures/cert.crt b/src/core_plugins/console/server/__tests__/fixtures/cert.crt new file mode 100644 index 000000000000000..360cdfaaaa5a968 --- /dev/null +++ b/src/core_plugins/console/server/__tests__/fixtures/cert.crt @@ -0,0 +1 @@ +test certificate diff --git a/src/core_plugins/console/server/__tests__/fixtures/cert.key b/src/core_plugins/console/server/__tests__/fixtures/cert.key new file mode 100644 index 000000000000000..04d3bfef24188d3 --- /dev/null +++ b/src/core_plugins/console/server/__tests__/fixtures/cert.key @@ -0,0 +1 @@ +test key diff --git a/src/core_plugins/console/server/elasticsearch_proxy_config.js b/src/core_plugins/console/server/elasticsearch_proxy_config.js new file mode 100644 index 000000000000000..823c04341449794 --- /dev/null +++ b/src/core_plugins/console/server/elasticsearch_proxy_config.js @@ -0,0 +1,54 @@ +import _ from 'lodash'; +import { readFileSync } from 'fs'; +import http from 'http'; +import https from 'https'; +import url from 'url'; + +const readFile = (file) => readFileSync(file, 'utf8'); + +const createAgent = (server) => { + const config = server.config(); + const target = url.parse(config.get('elasticsearch.url')); + + if (!/^https/.test(target.protocol)) return new http.Agent(); + + const agentOptions = {}; + + let verificationMode = config.get('elasticsearch.ssl.verificationMode'); + switch (verificationMode) { + case 'none': + agentOptions.rejectUnauthorized = false; + break; + case 'certificate': + agentOptions.rejectUnauthorized = true; + + // by default, NodeJS is checking the server identify + agentOptions.checkServerIdentity = _.noop; + break; + case 'full': + agentOptions.rejectUnauthorized = true; + break; + default: + throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); + } + + if (_.size(config.get('elasticsearch.ssl.certificateAuthorities'))) { + agentOptions.ca = config.get('elasticsearch.ssl.certificateAuthorities').map(readFile); + } + + // Add client certificate and key if required by elasticsearch + if (config.get('elasticsearch.ssl.certificate') && config.get('elasticsearch.ssl.key')) { + agentOptions.cert = readFile(config.get('elasticsearch.ssl.certificate')); + agentOptions.key = readFile(config.get('elasticsearch.ssl.key')); + agentOptions.passphrase = config.get('elasticsearch.ssl.keyPassphrase'); + } + + return new https.Agent(agentOptions); +}; + +export const getElasticsearchProxyConfig = (server) => { + return { + timeout: server.config().get('elasticsearch.requestTimeout'), + agent: createAgent(server) + }; +}; diff --git a/src/core_plugins/elasticsearch/__tests__/index.js b/src/core_plugins/elasticsearch/__tests__/index.js new file mode 100644 index 000000000000000..97d5e7b08ed2cbc --- /dev/null +++ b/src/core_plugins/elasticsearch/__tests__/index.js @@ -0,0 +1,93 @@ +import { Deprecations } from '../../../deprecation'; +import expect from 'expect.js'; +import index from '../index'; +import { noop } from 'lodash'; +import sinon from 'sinon'; + +describe('plugins/elasticsearch', function () { + describe('#deprecations()', function () { + let transformDeprecations; + + before(function () { + const Plugin = function (options) { + this.deprecations = options.deprecations; + }; + + const plugin = index({Plugin}); + + const deprecations = plugin.deprecations(Deprecations); + transformDeprecations = (settings, log = noop) => { + deprecations.forEach(deprecation => deprecation(settings, log)); + }; + }); + + context('verificationMode', function () { + it('sets ssl.verificationMode to none when verify is false', function () { + const settings = { + ssl: { + verify: false + } + }; + + transformDeprecations(settings); + expect(settings.ssl.verificationMode).to.be('none'); + expect(settings.ssl.verify).to.be(undefined); + }); + + it('should log when deprecating verify from false', function () { + const settings = { + ssl: { + verify: false + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.calledOnce).to.be(true); + }); + + it('sets ssl.verificationMode to full when verify is true', function () { + const settings = { + ssl: { + verify: true + } + }; + + transformDeprecations(settings); + expect(settings.ssl.verificationMode).to.be('full'); + expect(settings.ssl.verify).to.be(undefined); + }); + + it('should log when deprecating verify from true', function () { + const settings = { + ssl: { + verify: true + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.calledOnce).to.be(true); + }); + + it(`shouldn't set verificationMode when verify isn't present`, function () { + const settings = { + ssl: {} + }; + + transformDeprecations(settings); + expect(settings.ssl.verificationMode).to.be(undefined); + }); + + it(`shouldn't log when verify isn't present`, function () { + const settings = { + ssl: {} + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + }); + }); +}); diff --git a/src/core_plugins/elasticsearch/index.js b/src/core_plugins/elasticsearch/index.js index 46cce51c73d598f..d7a949ea32a3d03 100644 --- a/src/core_plugins/elasticsearch/index.js +++ b/src/core_plugins/elasticsearch/index.js @@ -1,4 +1,5 @@ -import { trim, trimRight } from 'lodash'; +import { get, has, set, trim, trimRight } from 'lodash'; +import { unset } from '../../utils'; import { methodNotAllowed } from 'boom'; import healthCheck from './lib/health_check'; @@ -27,15 +28,34 @@ module.exports = function ({ Plugin }) { pingTimeout: number().default(ref('requestTimeout')), startupTimeout: number().default(5000), ssl: object({ - verify: boolean().default(true), - ca: array().single().items(string()), - cert: string(), - key: string() + verificationMode: string().valid('none', 'certificate', 'full').default('full'), + certificateAuthorities: array().single().items(string()), + certificate: string(), + key: string(), + keyPassphrase: string() }).default(), apiVersion: Joi.string().default('master'), }).default(); }, + deprecations({ rename }) { + return [ + rename('ssl.ca', 'ssl.certificateAuthorities'), + rename('ssl.cert', 'ssl.certificate'), + (settings, log) => { + if (!has(settings, 'ssl.verify')) { + return; + } + + const verificationMode = get(settings, 'ssl.verify') ? 'full' : 'none'; + set(settings, 'ssl.verificationMode', verificationMode); + unset(settings, 'ssl.verify'); + + log(`Config key "ssl.verify" is deprecated. It has been replaced with "ssl.verificationMode"`); + } + ]; + }, + uiExports: { injectDefaultVars(server, options) { return { diff --git a/src/core_plugins/elasticsearch/lib/__tests__/create_agent.js b/src/core_plugins/elasticsearch/lib/__tests__/create_agent.js new file mode 100644 index 000000000000000..85c0e4747e141b8 --- /dev/null +++ b/src/core_plugins/elasticsearch/lib/__tests__/create_agent.js @@ -0,0 +1,114 @@ +import expect from 'expect.js'; +import createAgent from '../create_agent'; +import https from 'https'; +import http from 'http'; +import sinon from 'sinon'; + +describe('plugins/elasticsearch', function () { + describe('lib/create_agent', function () { + + let server; + + beforeEach(function () { + const stub = sinon.stub(); + server = { + config() { + return { + get: stub + }; + } + }; + + server.config().get.withArgs('elasticsearch.url').returns('http://localhost:9200'); + server.config().get.withArgs('elasticsearch.ssl.verificationMode').returns('full'); + }); + + const setElasticsearchConfig = (key, value) => { + server.config().get.withArgs(`elasticsearch.${key}`).returns(value); + }; + + it(`uses https.Agent when url's protocol is https`, function () { + setElasticsearchConfig('url', 'https://localhost:9200'); + const agent = createAgent(server); + expect(agent).to.be.a(https.Agent); + }); + + it(`uses http.Agent when url's protocol is http`, function () { + setElasticsearchConfig('url', 'http://localhost:9200'); + const agent = createAgent(server); + expect(agent).to.be.a(http.Agent); + }); + + context('ssl', function () { + beforeEach(function () { + setElasticsearchConfig('url', 'https://localhost:9200'); + }); + + it('sets rejectUnauthorized to false when verificationMode is none', function () { + setElasticsearchConfig('ssl.verificationMode', 'none'); + const agent = createAgent(server); + expect(agent.options.rejectUnauthorized).to.be(false); + }); + + it('sets rejectUnauthorized to true when verificationMode is certificate', function () { + setElasticsearchConfig('ssl.verificationMode', 'certificate'); + const agent = createAgent(server); + expect(agent.options.rejectUnauthorized).to.be(true); + }); + + it('sets checkServerIdentity to not check hostname when verificationMode is certificate', function () { + setElasticsearchConfig('ssl.verificationMode', 'certificate'); + const agent = createAgent(server); + + const cert = { + subject: { + CN: 'wrong.com' + } + }; + + expect(agent.options.checkServerIdentity).withArgs('right.com', cert).to.not.throwException(); + const result = agent.options.checkServerIdentity('right.com', cert); + expect(result).to.be(undefined); + }); + + it('sets rejectUnauthorized to true when verificationMode is full', function () { + setElasticsearchConfig('ssl.verificationMode', 'full'); + const agent = createAgent(server); + + expect(agent.options.rejectUnauthorized).to.be(true); + }); + + it(`doesn't set checkServerIdentity when verificationMode is full`, function () { + setElasticsearchConfig('ssl.verificationMode', 'full'); + const agent = createAgent(server); + + expect(agent.options.checkServerIdentity).to.be(undefined); + }); + + it(`sets ca when certificateAuthorities are specified`, function () { + setElasticsearchConfig('ssl.certificateAuthorities', [__dirname + '/fixtures/ca.crt']); + + const agent = createAgent(server); + expect(agent.options.ca).to.contain('test ca certificate\n'); + }); + + it(`sets cert and key when certificate and key paths are specified`, function () { + setElasticsearchConfig('ssl.certificate', __dirname + '/fixtures/cert.crt'); + setElasticsearchConfig('ssl.key', __dirname + '/fixtures/cert.key'); + + const agent = createAgent(server); + expect(agent.options.cert).to.be('test certificate\n'); + expect(agent.options.key).to.be('test key\n'); + }); + + it(`sets passphrase when certificate, key and keyPassphrase are specified`, function () { + setElasticsearchConfig('ssl.certificate', __dirname + '/fixtures/cert.crt'); + setElasticsearchConfig('ssl.key', __dirname + '/fixtures/cert.key'); + setElasticsearchConfig('ssl.keyPassphrase', 'secret'); + + const agent = createAgent(server); + expect(agent.options.passphrase).to.be('secret'); + }); + }); + }); +}); diff --git a/src/core_plugins/elasticsearch/lib/__tests__/fixtures/ca.crt b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/ca.crt new file mode 100644 index 000000000000000..075fdd038dafff8 --- /dev/null +++ b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/ca.crt @@ -0,0 +1 @@ +test ca certificate diff --git a/src/core_plugins/elasticsearch/lib/__tests__/fixtures/cert.crt b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/cert.crt new file mode 100644 index 000000000000000..360cdfaaaa5a968 --- /dev/null +++ b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/cert.crt @@ -0,0 +1 @@ +test certificate diff --git a/src/core_plugins/elasticsearch/lib/__tests__/fixtures/cert.key b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/cert.key new file mode 100644 index 000000000000000..04d3bfef24188d3 --- /dev/null +++ b/src/core_plugins/elasticsearch/lib/__tests__/fixtures/cert.key @@ -0,0 +1 @@ +test key diff --git a/src/core_plugins/elasticsearch/lib/create_agent.js b/src/core_plugins/elasticsearch/lib/create_agent.js index 9297fa637529acd..4efc2e4052f7be5 100644 --- a/src/core_plugins/elasticsearch/lib/create_agent.js +++ b/src/core_plugins/elasticsearch/lib/create_agent.js @@ -10,18 +10,35 @@ module.exports = _.memoize(function (server) { if (!/^https/.test(target.protocol)) return new http.Agent(); - const agentOptions = { - rejectUnauthorized: config.get('elasticsearch.ssl.verify') - }; + const agentOptions = {}; - if (_.size(config.get('elasticsearch.ssl.ca'))) { - agentOptions.ca = config.get('elasticsearch.ssl.ca').map(readFile); + let verificationMode = config.get('elasticsearch.ssl.verificationMode'); + switch (verificationMode) { + case 'none': + agentOptions.rejectUnauthorized = false; + break; + case 'certificate': + agentOptions.rejectUnauthorized = true; + + // by default, NodeJS is checking the server identify + agentOptions.checkServerIdentity = _.noop; + break; + case 'full': + agentOptions.rejectUnauthorized = true; + break; + default: + throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); + } + + if (_.size(config.get('elasticsearch.ssl.certificateAuthorities'))) { + agentOptions.ca = config.get('elasticsearch.ssl.certificateAuthorities').map(readFile); } // Add client certificate and key if required by elasticsearch - if (config.get('elasticsearch.ssl.cert') && config.get('elasticsearch.ssl.key')) { - agentOptions.cert = readFile(config.get('elasticsearch.ssl.cert')); + if (config.get('elasticsearch.ssl.certificate') && config.get('elasticsearch.ssl.key')) { + agentOptions.cert = readFile(config.get('elasticsearch.ssl.certificate')); agentOptions.key = readFile(config.get('elasticsearch.ssl.key')); + agentOptions.passphrase = config.get('elasticsearch.ssl.keyPassphrase'); } return new https.Agent(agentOptions); diff --git a/src/core_plugins/elasticsearch/lib/expose_client.js b/src/core_plugins/elasticsearch/lib/expose_client.js index a1748e54ebbebe5..7dbb829f79ab8de 100644 --- a/src/core_plugins/elasticsearch/lib/expose_client.js +++ b/src/core_plugins/elasticsearch/lib/expose_client.js @@ -28,10 +28,11 @@ module.exports = function (server) { url: config.get('elasticsearch.url'), username: config.get('elasticsearch.username'), password: config.get('elasticsearch.password'), - verifySsl: config.get('elasticsearch.ssl.verify'), - clientCrt: config.get('elasticsearch.ssl.cert'), + sslVerificationMode: config.get('elasticsearch.ssl.verificationMode'), + clientCrt: config.get('elasticsearch.ssl.certificate'), clientKey: config.get('elasticsearch.ssl.key'), - ca: config.get('elasticsearch.ssl.ca'), + clientKeyPassphrase: config.get('elasticsearch.ssl.keyPassphrase'), + ca: config.get('elasticsearch.ssl.certificateAuthorities'), apiVersion: config.get('elasticsearch.apiVersion'), pingTimeout: config.get('elasticsearch.pingTimeout'), requestTimeout: config.get('elasticsearch.requestTimeout'), @@ -46,10 +47,29 @@ module.exports = function (server) { uri.auth = util.format('%s:%s', options.username, options.password); } - const ssl = { rejectUnauthorized: options.verifySsl }; + const ssl = { }; + + switch (options.sslVerificationMode) { + case 'none': + ssl.rejectUnauthorized = false; + break; + case 'certificate': + ssl.rejectUnauthorized = true; + + // by default, NodeJS is checking the server identify + ssl.checkServerIdentity = _.noop; + break; + case 'full': + ssl.rejectUnauthorized = true; + break; + default: + throw new Error(`Unknown ssl verificationMode: ${options.sslVerificationMode}`); + } + if (options.clientCrt && options.clientKey) { ssl.cert = readFile(options.clientCrt); ssl.key = readFile(options.clientKey); + ssl.passphrase = options.clientKeyPassphrase; } if (options.ca) { ssl.ca = options.ca.map(readFile); diff --git a/src/deprecation/__tests__/create_transform.js b/src/deprecation/__tests__/create_transform.js new file mode 100644 index 000000000000000..7bb333ca6bd8786 --- /dev/null +++ b/src/deprecation/__tests__/create_transform.js @@ -0,0 +1,39 @@ +import createTransform from '../create_transform'; +import expect from 'expect.js'; +import { noop } from 'lodash'; +import sinon from 'sinon'; + +describe('deprecation', function () { + describe('createTransform', function () { + it(`doesn't modify settings parameter`, function () { + const settings = { + original: true + }; + const deprecations = [(settings) => { + settings.origial = false; + }]; + createTransform(deprecations)(settings); + expect(settings.original).to.be(true); + }); + + it('calls single deprecation in array', function () { + const deprecations = [sinon.spy()]; + createTransform(deprecations)({}); + expect(deprecations[0].calledOnce).to.be(true); + }); + + it('calls multiple deprecations in array', function () { + const deprecations = [sinon.spy(), sinon.spy()]; + createTransform(deprecations)({}); + expect(deprecations[0].calledOnce).to.be(true); + expect(deprecations[1].calledOnce).to.be(true); + }); + + it('passes log function to deprecation', function () { + const deprecation = sinon.spy(); + const log = function () {}; + createTransform([deprecation])({}, log); + expect(deprecation.args[0][1]).to.be(log); + }); + }); +}); diff --git a/src/deprecation/create_transform.js b/src/deprecation/create_transform.js new file mode 100644 index 000000000000000..c932796c1edcdba --- /dev/null +++ b/src/deprecation/create_transform.js @@ -0,0 +1,14 @@ +import { deepCloneWithBuffers as clone } from '../utils'; +import { forEach, noop } from 'lodash'; + +export default function (deprecations) { + return (settings, log = noop) => { + const result = clone(settings); + + forEach(deprecations, (deprecation) => { + deprecation(result, log); + }); + + return result; + }; +} diff --git a/src/deprecation/deprecations/__tests__/rename.js b/src/deprecation/deprecations/__tests__/rename.js new file mode 100644 index 000000000000000..a5688b5eaee7139 --- /dev/null +++ b/src/deprecation/deprecations/__tests__/rename.js @@ -0,0 +1,65 @@ +import expect from 'expect.js'; +import { noop } from 'lodash'; +import rename from '../rename'; +import sinon from 'sinon'; + +describe('deprecation/deprecations', function () { + describe('rename', function () { + it('should rename simple property', function () { + const value = 'value'; + const settings = { + before: value + }; + + rename('before', 'after')(settings); + expect(settings.before).to.be(undefined); + expect(settings.after).to.be(value); + }); + + it ('should rename nested property', function () { + const value = 'value'; + const settings = { + someObject: { + before: value + } + }; + + rename('someObject.before', 'someObject.after')(settings); + expect(settings.someObject.before).to.be(undefined); + expect(settings.someObject.after).to.be(value); + }); + + it ('should rename property, even when the value is null', function () { + const value = null; + const settings = { + before: value + }; + + rename('before', 'after')(settings); + expect(settings.before).to.be(undefined); + expect(settings.after).to.be(null); + }); + + it (`shouldn't log when a rename doesn't occur`, function () { + const settings = { + exists: true + }; + + const log = sinon.spy(); + rename('doesntExist', 'alsoDoesntExist')(settings, log); + expect(log.called).to.be(false); + }); + + it ('should log when a rename does occur', function () { + const settings = { + exists: true + }; + + const log = sinon.spy(); + rename('exists', 'alsoExists')(settings, log); + + expect(log.calledOnce).to.be(true); + expect(log.args[0][0]).to.match(/exists.+deprecated/); + }); + }); +}); diff --git a/src/deprecation/deprecations/__tests__/unused.js b/src/deprecation/deprecations/__tests__/unused.js new file mode 100644 index 000000000000000..27b5f1efeab4b42 --- /dev/null +++ b/src/deprecation/deprecations/__tests__/unused.js @@ -0,0 +1,57 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import unused from '../unused'; + +describe('deprecation/deprecations', function () { + describe('unused', function () { + it('should remove unused setting', function () { + const settings = { + old: true + }; + + unused('old')(settings); + expect(settings.old).to.be(undefined); + }); + + it(`shouldn't remove used setting`, function () { + const value = 'value'; + const settings = { + new: value + }; + + unused('old')(settings); + expect(settings.new).to.be(value); + }); + + it('should remove unused setting, even when null', function () { + const settings = { + old: null + }; + + unused('old')(settings); + expect(settings.old).to.be(undefined); + }); + + it('should log when removing unused setting', function () { + const settings = { + old: true + }; + + const log = sinon.spy(); + unused('old')(settings, log); + + expect(log.calledOnce).to.be(true); + expect(log.args[0][0]).to.match(/old.+deprecated/); + }); + + it(`shouldn't log when no setting is unused`, function () { + const settings = { + new: true + }; + + const log = sinon.spy(); + unused('old')(settings, log); + expect(log.called).to.be(false); + }); + }); +}); diff --git a/src/deprecation/deprecations/index.js b/src/deprecation/deprecations/index.js new file mode 100644 index 000000000000000..03e9b892a23722c --- /dev/null +++ b/src/deprecation/deprecations/index.js @@ -0,0 +1,2 @@ +export rename from './rename'; +export unused from './unused'; diff --git a/src/deprecation/deprecations/rename.js b/src/deprecation/deprecations/rename.js new file mode 100644 index 000000000000000..9e17a860ebab8a6 --- /dev/null +++ b/src/deprecation/deprecations/rename.js @@ -0,0 +1,16 @@ +import { get, isUndefined, isNull, noop, set } from 'lodash'; +import { unset } from '../../utils'; + +export default function (oldKey, newKey) { + return (settings, log = noop) => { + const value = get(settings, oldKey); + if (isUndefined(value)) { + return; + } + + unset(settings, oldKey); + set(settings, newKey, value); + + log(`Config key "${oldKey}" is deprecated. It has been replaced with "${newKey}"`); + }; +} diff --git a/src/deprecation/deprecations/unused.js b/src/deprecation/deprecations/unused.js new file mode 100644 index 000000000000000..e226d01a357c431 --- /dev/null +++ b/src/deprecation/deprecations/unused.js @@ -0,0 +1,14 @@ +import { get, isUndefined, isNull, noop } from 'lodash'; +import { unset } from '../../utils'; + +export default function (oldKey) { + return (settings, log = noop) => { + const value = get(settings, oldKey); + if (isUndefined(value)) { + return; + } + + unset(settings, oldKey); + log(`${oldKey} is deprecated and is no longer used`); + }; +} diff --git a/src/deprecation/index.js b/src/deprecation/index.js new file mode 100644 index 000000000000000..5ffc6eefab7d14d --- /dev/null +++ b/src/deprecation/index.js @@ -0,0 +1,2 @@ +export createTransform from './create_transform'; +export * as Deprecations from './deprecations'; diff --git a/src/server/config/__tests__/complete.js b/src/server/config/__tests__/complete.js new file mode 100644 index 000000000000000..b5c7da5f557d143 --- /dev/null +++ b/src/server/config/__tests__/complete.js @@ -0,0 +1,99 @@ +import complete from '../complete'; +import expect from 'expect.js'; +import { noop } from 'lodash'; +import sinon from 'sinon'; + +describe('server config complete', function () { + it(`should call server.log when there's an unused setting`, function () { + const kbnServer = { + settings: { + unused: true + } + }; + + const server = { + decorate: noop, + log: sinon.spy() + }; + + const config = { + get: sinon.stub().returns({ + used: true + }) + }; + + complete(kbnServer, server, config); + + expect(server.log.calledOnce).to.be(true); + }); + + it(`shouldn't call server.log when there isn't an unused setting`, function () { + const kbnServer = { + settings: { + used: true + } + }; + + const server = { + decorate: noop, + log: sinon.spy() + }; + + const config = { + get: sinon.stub().returns({ + used: true + }) + }; + + complete(kbnServer, server, config); + + expect(server.log.called).to.be(false); + }); + + it(`shouldn't call server.log when there are more config values than settings`, function () { + const kbnServer = { + settings: { + used: true + } + }; + + const server = { + decorate: noop, + log: sinon.spy() + }; + + const config = { + get: sinon.stub().returns({ + used: true, + foo: 'bar' + }) + }; + + complete(kbnServer, server, config); + expect(server.log.called).to.be(false); + }); + + it('should transform deprecated settings ', function () { + const kbnServer = { + settings: { + port: 8080 + } + }; + + const server = { + decorate: noop, + log: sinon.spy() + }; + + const config = { + get: sinon.stub().returns({ + server: { + port: 8080 + } + }) + }; + + complete(kbnServer, server, config); + expect(server.log.called).to.be(false); + }); +}); diff --git a/src/server/config/__tests__/config.js b/src/server/config/__tests__/config.js index 766942ba0245eef..4f4bd178e482154 100644 --- a/src/server/config/__tests__/config.js +++ b/src/server/config/__tests__/config.js @@ -216,15 +216,15 @@ describe('lib/config/config', function () { }); it('should allow you to extend the schema at the top level', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema('myTest', newSchema); + let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); + config.extendSchema(newSchema, {}, 'myTest'); expect(config.get('myTest.test')).to.be(true); }); it('should allow you to extend the schema with a prefix', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema('prefix.myTest', newSchema); - expect(config.get('prefix')).to.eql({ myTest: { test: true } }); + let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); + config.extendSchema(newSchema, {}, 'prefix.myTest'); + expect(config.get('prefix')).to.eql({ myTest: { test: true }}); expect(config.get('prefix.myTest')).to.eql({ test: true }); expect(config.get('prefix.myTest.test')).to.be(true); }); diff --git a/src/server/config/__tests__/schema.js b/src/server/config/__tests__/schema.js index 3c0cd25536d79fe..1d0a0202da7b311 100644 --- a/src/server/config/__tests__/schema.js +++ b/src/server/config/__tests__/schema.js @@ -1,6 +1,7 @@ import schemaProvider from '../schema'; import expect from 'expect.js'; import Joi from 'joi'; +import { set } from 'lodash'; describe('Config schema', function () { let schema; @@ -11,6 +12,11 @@ describe('Config schema', function () { } describe('server', function () { + it('everything is optional', function () { + const { error } = validate({}); + expect(error).to.be(null); + }); + describe('basePath', function () { it('accepts empty strings', function () { const { error } = validate({ server: { basePath: '' } }); @@ -33,6 +39,163 @@ describe('Config schema', function () { expect(error).to.have.property('details'); expect(error.details[0]).to.have.property('path', 'server.basePath'); }); + + }); + + describe('ssl', function () { + describe('enabled', function () { + + it('can\'t be a string', function () { + const config = {}; + set(config, 'server.ssl.enabled', 'bogus'); + const { error } = validate(config); + expect(error).to.be.an(Object); + expect(error).to.have.property('details'); + expect(error.details[0]).to.have.property('path', 'server.ssl.enabled'); + }); + + it('can be true', function () { + const config = {}; + set(config, 'server.ssl.enabled', true); + set(config, 'server.ssl.certificate', '/path.cert'); + set(config, 'server.ssl.key', '/path.key'); + const { error } = validate(config); + expect(error).to.be(null); + }); + + it('can be false', function () { + const config = {}; + set(config, 'server.ssl.enabled', false); + const { error } = validate(config); + expect(error).to.be(null); + }); + }); + + describe('certificate', function () { + + it('isn\'t required when ssl isn\'t enabled', function () { + const config = {}; + set(config, 'server.ssl.enabled', false); + const { error } = validate(config); + expect(error).to.be(null); + }); + + it('is required when ssl is enabled', function () { + const config = {}; + set(config, 'server.ssl.enabled', true); + set(config, 'server.ssl.key', '/path.key'); + const { error } = validate(config); + expect(error).to.be.an(Object); + expect(error).to.have.property('details'); + expect(error.details[0]).to.have.property('path', 'server.ssl.certificate'); + }); + }); + + describe('key', function () { + it('isn\'t required when ssl isn\'t enabled', function () { + const config = {}; + set(config, 'server.ssl.enabled', false); + const { error } = validate(config); + expect(error).to.be(null); + }); + + it('is required when ssl is enabled', function () { + const config = {}; + set(config, 'server.ssl.enabled', true); + set(config, 'server.ssl.certificate', '/path.cert'); + const { error } = validate(config); + expect(error).to.be.an(Object); + expect(error).to.have.property('details'); + expect(error.details[0]).to.have.property('path', 'server.ssl.key'); + }); + }); + + describe('keyPassphrase', function () { + it ('is a possible config value', function () { + const config = {}; + set(config, 'server.ssl.keyPassphrase', 'password'); + const { error } = validate(config); + expect(error).to.be(null); + }); + }); + + describe('clientAuthentication', function () { + it ('defaults to \'none\'', function () { + const config = {}; + const { error, value } = validate({}); + expect(error).to.be(null); + expect(value).to.be.an(Object); + expect(value.server).to.be.an(Object); + expect(value.server.ssl).to.be.an(Object); + expect(value.server.ssl.clientAuthentication).to.be('none'); + }); + + ['none', 'optional', 'required'].forEach((option) => { + it(`allows ${option}`, function () { + const config = {}; + set(config, 'server.ssl.clientAuthentication', option); + const { error } = validate(config); + expect(error).to.be(null); + }); + }); + + ['bogus', 'somethingelse'].forEach((option) => { + it(`rejects ${option}`, function () { + const config = {}; + set(config, 'server.ssl.clientAuthentication', option); + const { error } = validate(config); + expect(error).to.be.an(Object); + expect(error).to.have.property('details'); + expect(error.details[0]).to.have.property('path', 'server.ssl.clientAuthentication'); + }); + }); + + }); + + describe('certificateAuthorities', function () { + it('allows array of string', function () { + const config = {}; + set(config, 'server.ssl.certificateAuthorities', ['/path1.crt', '/path2.crt']); + const { error } = validate(config); + expect(error).to.be(null); + }); + + it('allows a single string', function () { + const config = {}; + set(config, 'server.ssl.certificateAuthorities', '/path1.crt'); + const { error } = validate(config); + expect(error).to.be(null); + }); + }); + + describe('supportedProtocols', function () { + + it ('rejects SSLv2', function () { + const config = {}; + set(config, 'server.ssl.supportedProtocols', ['SSLv2']); + const { error } = validate(config); + expect(error).to.be.an(Object); + expect(error).to.have.property('details'); + expect(error.details[0]).to.have.property('path', 'server.ssl.supportedProtocols.0'); + }); + + it('rejects SSLv3', function () { + const config = {}; + set(config, 'server.ssl.supportedProtocols', ['SSLv3']); + const { error } = validate(config); + expect(error).to.be.an(Object); + expect(error).to.have.property('details'); + expect(error.details[0]).to.have.property('path', 'server.ssl.supportedProtocols.0'); + }); + + it('accepts TLSv1, TLSv1.1, TLSv1.2', function () { + const config = {}; + set(config, 'server.ssl.supportedProtocols', ['TLSv1', 'TLSv1.1', 'TLSv1.2']); + const { error } = validate(config); + expect(error).to.be(null); + }); + }); }); + }); }); diff --git a/src/server/config/__tests__/transform_deprecations.js b/src/server/config/__tests__/transform_deprecations.js new file mode 100644 index 000000000000000..94c97a31a78ff4f --- /dev/null +++ b/src/server/config/__tests__/transform_deprecations.js @@ -0,0 +1,61 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import { transformDeprecations } from '../transform_deprecations'; + +describe('server/config', function () { + describe('transformDeprecations', function () { + describe('server.ssl.enabled', function () { + it('sets enabled to true when certificate and key are set', function () { + const settings = { + server: { + ssl: { + certificate: '/cert.crt', + key: '/key.key' + } + } + }; + + const result = transformDeprecations(settings); + expect(result.server.ssl.enabled).to.be(true); + }); + + it('logs a message when automatically setting enabled to true', function () { + const settings = { + server: { + ssl: { + certificate: '/cert.crt', + key: '/key.key' + } + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.calledOnce).to.be(true); + }); + + it(`doesn't set enabled when key and cert aren't set`, function () { + const settings = { + server: { + ssl: {} + } + }; + + const result = transformDeprecations(settings); + expect(result.server.ssl.enabled).to.be(undefined); + }); + + it(`doesn't log a message when not automatically setting enabled`, function () { + const settings = { + server: { + ssl: {} + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + }); + }); +}); diff --git a/src/server/config/complete.js b/src/server/config/complete.js index a76edae6f9a95ae..0831016cd2569b4 100644 --- a/src/server/config/complete.js +++ b/src/server/config/complete.js @@ -1,11 +1,17 @@ +import { difference, keys } from 'lodash'; +import { transformDeprecations } from './transform_deprecations'; + +const getUnusedSettings = (settings, configValues) => { + return difference(keys(transformDeprecations(settings)), keys(configValues)); +}; + export default function (kbnServer, server, config) { server.decorate('server', 'config', function () { return kbnServer.config; }); - const tmpl = 'Settings for "<%= key %>" were not applied, check for spelling errors and ensure the plugin is loaded.'; - for (const [key, val] of config.getPendingSets()) { - server.log(['warning', 'config'], { key, val, tmpl }); + for (const key of getUnusedSettings(kbnServer.settings, config.get())) { + server.log(['warning', 'config'], `Settings for "${key}" were not applied, check for spelling errors and ensure the plugin is loaded.`); } } diff --git a/src/server/config/config.js b/src/server/config/config.js index db6b3520b776c77..1d036ca63e96a05 100644 --- a/src/server/config/config.js +++ b/src/server/config/config.js @@ -1,15 +1,13 @@ import Joi from 'joi'; import _ from 'lodash'; import override from './override'; -import unset from './unset'; import createDefaultSchema from './schema'; -import pkg from '../../utils/package_json'; -import clone from './deep_clone_with_buffers'; +import { pkg, unset } from '../../utils'; +import { deepCloneWithBuffers as clone } from '../../utils'; const schema = Symbol('Joi Schema'); const schemaExts = Symbol('Schema Extensions'); const vals = Symbol('config values'); -const pendingSets = Symbol('Pending Settings'); module.exports = class Config { static withDefaultSchema(settings = {}) { @@ -19,19 +17,18 @@ module.exports = class Config { constructor(initialSchema, initialSettings) { this[schemaExts] = Object.create(null); this[vals] = Object.create(null); - this[pendingSets] = _.merge(Object.create(null), initialSettings || {}); - if (initialSchema) this.extendSchema(initialSchema); + this.extendSchema(initialSchema, initialSettings); } - getPendingSets() { - return new Map(_.pairs(this[pendingSets])); - } + extendSchema(extension, settings, key) { + if (!extension) { + return; + } - extendSchema(key, extension) { - if (key && key.isJoi) { - return _.each(key._inner.children, (child) => { - this.extendSchema(child.key, child.schema); + if (!key) { + return _.each(extension._inner.children, (child) => { + this.extendSchema(child.schema, _.get(settings, child.key), child.key); }); } @@ -42,13 +39,7 @@ module.exports = class Config { _.set(this[schemaExts], key, extension); this[schema] = null; - const initialVals = _.get(this[pendingSets], key); - if (initialVals) { - this.set(key, initialVals); - unset(this[pendingSets], key); - } else { - this._commit(this[vals]); - } + this.set(key, settings); } removeSchema(key) { @@ -58,7 +49,6 @@ module.exports = class Config { this[schema] = null; unset(this[schemaExts], key); - unset(this[pendingSets], key); unset(this[vals], key); } diff --git a/src/server/config/deprecation_warnings.js b/src/server/config/deprecation_warnings.js new file mode 100644 index 000000000000000..c0c1d5c2c97f381 --- /dev/null +++ b/src/server/config/deprecation_warnings.js @@ -0,0 +1,7 @@ +import { transformDeprecations } from './transform_deprecations'; + +export default function (kbnServer, server) { + transformDeprecations(kbnServer.settings, (message) => { + server.log(['warning', 'config', 'deprecation'], message); + }); +} diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 957afe2f869ce52..c3a9108457016b3 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -41,8 +41,19 @@ module.exports = () => Joi.object({ defaultRoute: Joi.string().default('/app/kibana').regex(/^\//, `start with a slash`), basePath: Joi.string().default('').allow('').regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), ssl: Joi.object({ - cert: Joi.string(), - key: Joi.string() + enabled: Joi.boolean().default(false), + certificate: Joi.string().when('enabled', { + is: true, + then: Joi.required(), + }), + key: Joi.string().when('enabled', { + is: true, + then: Joi.required() + }), + keyPassphrase: Joi.string(), + certificateAuthorities: Joi.array().single().items(Joi.string()), + clientAuthentication: Joi.string().valid('none', 'optional', 'required').default('none'), + supportedProtocols: Joi.array().items(Joi.string().valid('TLSv1', 'TLSv1.1', 'TLSv1.2')) }).default(), cors: Joi.when('$dev', { is: true, diff --git a/src/server/config/setup.js b/src/server/config/setup.js index dcecb0c2abb796c..c49ef03681a4729 100644 --- a/src/server/config/setup.js +++ b/src/server/config/setup.js @@ -1,4 +1,7 @@ import Config from './config'; +import { transformDeprecations } from './transform_deprecations'; + module.exports = function (kbnServer) { - kbnServer.config = Config.withDefaultSchema(kbnServer.settings); + const settings = transformDeprecations(kbnServer.settings); + kbnServer.config = Config.withDefaultSchema(settings); }; diff --git a/src/server/config/transform_deprecations.js b/src/server/config/transform_deprecations.js new file mode 100644 index 000000000000000..c4b498971215cfb --- /dev/null +++ b/src/server/config/transform_deprecations.js @@ -0,0 +1,56 @@ +import _ , { partial } from 'lodash'; +import { createTransform, Deprecations } from '../../deprecation'; + +const { rename, unused } = Deprecations; + +const serverSslEnabled = (settings, log) => { + const has = partial(_.has, settings); + const set = partial(_.set, settings); + + if (!has('server.ssl.enabled') && has('server.ssl.certificate') && has('server.ssl.key')) { + set('server.ssl.enabled', true); + log('Enabling ssl by only specifying server.ssl.certificate and server.ssl.key is deprecated. Please set server.ssl.enabled to true'); + } +}; + +const deprecations = [ + //server + rename('port' ,'server.port'), + rename('host', 'server.host'), + rename('pid_file', 'pid.file'), + rename('ssl_cert_file', 'server.ssl.certificate'), + rename('server.ssl.cert', 'server.ssl.certificate'), + rename('ssl_key_file', 'server.ssl.key'), + unused('server.xsrf.token'), + serverSslEnabled, + + // logging + rename('log_file', 'logging.dest'), + + // kibana + rename('kibana_index', 'kibana.index'), + rename('default_app_id', 'kibana.defaultAppId'), + + // es + rename('ca', 'elasticsearch.ssl.ca'), + rename('elasticsearch_preserve_host', 'elasticsearch.preserveHost'), + rename('elasticsearch_url', 'elasticsearch.url'), + rename('kibana_elasticsearch_client_crt', 'elasticsearch.ssl.cert'), + rename('kibana_elasticsearch_client_key', 'elasticsearch.ssl.key'), + rename('kibana_elasticsearch_password', 'elasticsearch.password'), + rename('kibana_elasticsearch_username', 'elasticsearch.username'), + rename('ping_timeout', 'elasticsearch.pingTimeout'), + rename('request_timeout', 'elasticsearch.requestTimeout'), + rename('shard_timeout', 'elasticsearch.shardTimeout'), + rename('startup_timeout', 'elasticsearch.startupTimeout'), + rename('verify_ssl', 'elasticsearch.ssl.verify'), + + // tilemap + rename('tilemap_url', 'tilemap.url'), + rename('tilemap_min_zoom', 'tilemap.options.minZoom'), + rename('tilemap_max_zoom', 'tilemap.options.maxZoom'), + rename('tilemap_attribution', 'tilemap.options.attribution'), + rename('tilemap_subdomains', 'tilemap.options.subdomains') +]; + +export const transformDeprecations = createTransform(deprecations); diff --git a/src/server/http/__tests__/secure_options.js b/src/server/http/__tests__/secure_options.js new file mode 100644 index 000000000000000..7f704d3ffc4cd07 --- /dev/null +++ b/src/server/http/__tests__/secure_options.js @@ -0,0 +1,28 @@ +import expect from 'expect.js'; +import secureOptions from '../secure_options'; +import crypto from 'crypto'; + +const constants = crypto.constants; + +describe('secure_options', function () { + it('allows null', function () { + expect(secureOptions(null)).to.be(null); + }); + + it ('allows an empty array', function () { + expect(secureOptions([])).to.be(null); + }); + + it ('removes TLSv1 if we only support TLSv1.1 and TLSv1.2', function () { + expect(secureOptions(['TLSv1.1', 'TLSv1.2'])).to.be(constants.SSL_OP_NO_TLSv1); + }); + + it ('removes TLSv1.1 and TLSv1.2 if we only support TLSv1', function () { + expect(secureOptions(['TLSv1'])).to.be(constants.SSL_OP_NO_TLSv1_1 | constants.SSL_OP_NO_TLSv1_2); + }); + + it ('removes TLSv1 and TLSv1.1 if we only support TLSv1.2', function () { + expect(secureOptions(['TLSv1.2'])).to.be(constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1); + }); + +}); diff --git a/src/server/http/secure_options.js b/src/server/http/secure_options.js new file mode 100644 index 000000000000000..3d80b7738ce9540 --- /dev/null +++ b/src/server/http/secure_options.js @@ -0,0 +1,24 @@ +import crypto from 'crypto'; +import { chain } from 'lodash'; + +const constants = crypto.constants; + +const protocolMap = { + TLSv1: crypto.constants.SSL_OP_NO_TLSv1, + 'TLSv1.1': crypto.constants.SSL_OP_NO_TLSv1_1, + 'TLSv1.2': crypto.constants.SSL_OP_NO_TLSv1_2 +}; + +export default function (supportedProtocols) { + if (!supportedProtocols || !supportedProtocols.length) { + return null; + } + + return chain(protocolMap) + .omit(supportedProtocols) + .values() + .reduce(function (value, sum) { + return value | sum; + }, 0) + .value(); +} diff --git a/src/server/http/setup_connection.js b/src/server/http/setup_connection.js index 736d38678972d79..6353a01d40b49e4 100644 --- a/src/server/http/setup_connection.js +++ b/src/server/http/setup_connection.js @@ -1,9 +1,32 @@ import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import httpolyglot from 'httpolyglot'; - +import { map } from 'lodash'; +import secureOptions from './secure_options'; import tlsCiphers from './tls_ciphers'; +const getClientAuthenticationHttpOptions = (clientAuthentication) => { + switch (clientAuthentication) { + case 'none': + return { + requestCert: false, + rejectUnauthorized: false + }; + case 'optional': + return { + requestCert: true, + rejectUnauthorized: false + }; + case 'required': + return { + requestCert: true, + rejectUnauthorized: true + }; + default: + throw new Error(`Unknown clientAuthentication option: ${clientAuthentication}`); + } +}; + export default function (kbnServer, server, config) { // this mixin is used outside of the kbn server, so it MUST work without a full kbnServer object. kbnServer = null; @@ -25,8 +48,7 @@ export default function (kbnServer, server, config) { } }; - // enable tlsOpts if ssl key and cert are defined - const useSsl = config.get('server.ssl.key') && config.get('server.ssl.cert'); + const useSsl = config.get('server.ssl.enabled'); // not using https? well that's easy! if (!useSsl) { @@ -34,16 +56,23 @@ export default function (kbnServer, server, config) { return; } + const { requestCert, rejectUnauthorized } = getClientAuthenticationHttpOptions(config.get('server.ssl.clientAuthentication')); + server.connection({ ...connectionOptions, tls: true, listener: httpolyglot.createServer({ key: readFileSync(config.get('server.ssl.key')), - cert: readFileSync(config.get('server.ssl.cert')), + cert: readFileSync(config.get('server.ssl.certificate')), + ca: map(config.get('server.ssl.certificateAuthorities'), readFileSync), + passphrase: config.get('server.ssl.keyPassphrase'), ciphers: tlsCiphers, // We use the server's cipher order rather than the client's to prevent the BEAST attack - honorCipherOrder: true + honorCipherOrder: true, + requestCert, + rejectUnauthorized, + secureOptions: secureOptions(config.get('server.ssl.supportedProtocols')) }) }); diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 4d91dfcd8d9ff02..0523f08863c72f8 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -21,6 +21,7 @@ module.exports = class KbnServer { require('./logging'), require('./warnings'), require('./status'), + require('./config/deprecation_warnings'), // writes pid file require('./pid'), diff --git a/src/server/plugins/plugin.js b/src/server/plugins/plugin.js index 1af3806cb3f6e5f..5eb12cddd367465 100644 --- a/src/server/plugins/plugin.js +++ b/src/server/plugins/plugin.js @@ -3,6 +3,7 @@ import Joi from 'joi'; import Bluebird, { attempt, fromNode } from 'bluebird'; import { basename, resolve } from 'path'; import { inherits } from 'util'; +import { Deprecations } from '../../deprecation'; const extendInitFns = Symbol('extend plugin initialization'); @@ -68,6 +69,7 @@ module.exports = class Plugin { this.externalInit = opts.init || _.noop; this.configPrefix = opts.configPrefix || this.id; this.getExternalConfigSchema = opts.config || _.noop; + this.getExternalDeprecations = opts.deprecations || _.noop; this.preInit = _.once(this.preInit); this.init = _.once(this.init); this[extendInitFns] = []; @@ -99,6 +101,11 @@ module.exports = class Plugin { return schema || defaultConfigSchema; } + getDeprecations() { + let rules = this.getExternalDeprecations(Deprecations); + return rules || []; + } + async preInit() { return await this.externalPreInit(this.kbnServer.server); } diff --git a/src/server/plugins/plugin_collection.js b/src/server/plugins/plugin_collection.js index 2bddd6dc4bb25ed..2d479dedcbd79cc 100644 --- a/src/server/plugins/plugin_collection.js +++ b/src/server/plugins/plugin_collection.js @@ -4,14 +4,24 @@ import { inspect } from 'util'; import { get, indexBy } from 'lodash'; import toPath from 'lodash/internal/toPath'; import Collection from '../../utils/collection'; +import { transformDeprecations } from '../config/transform_deprecations'; +import { createTransform } from '../../deprecation'; const byIdCache = Symbol('byIdCache'); const pluginApis = Symbol('pluginApis'); async function addPluginConfig(pluginCollection, plugin) { + const { config, server, settings } = pluginCollection.kbnServer; + + const transformedSettings = transformDeprecations(settings); + const pluginSettings = get(transformedSettings, plugin.configPrefix); + const deprecations = plugin.getDeprecations(); + const transformedPluginSettings = createTransform(deprecations)(pluginSettings, (message) => { + server.log(['warning', plugin.configPrefix, 'config', 'deprecation'], message); + }); + const configSchema = await plugin.getConfigSchema(); - const { config } = pluginCollection.kbnServer; - config.extendSchema(plugin.configPrefix, configSchema); + config.extendSchema(configSchema, transformedPluginSettings, plugin.configPrefix); } function removePluginConfig(pluginCollection, plugin) { diff --git a/src/server/config/__tests__/deep_clone_with_buffers.js b/src/utils/__tests__/deep_clone_with_buffers.js similarity index 100% rename from src/server/config/__tests__/deep_clone_with_buffers.js rename to src/utils/__tests__/deep_clone_with_buffers.js diff --git a/src/server/config/__tests__/unset.js b/src/utils/__tests__/unset.js similarity index 100% rename from src/server/config/__tests__/unset.js rename to src/utils/__tests__/unset.js diff --git a/src/server/config/deep_clone_with_buffers.js b/src/utils/deep_clone_with_buffers.js similarity index 100% rename from src/server/config/deep_clone_with_buffers.js rename to src/utils/deep_clone_with_buffers.js diff --git a/src/utils/index.js b/src/utils/index.js index d2899f988bf8907..504a74923046c63 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,4 +1,6 @@ export Binder from './binder'; export BinderFor from './binder_for'; +export deepCloneWithBuffers from './deep_clone_with_buffers'; export fromRoot from './from_root'; export pkg from './package_json'; +export unset from './unset'; diff --git a/src/server/config/unset.js b/src/utils/unset.js similarity index 100% rename from src/server/config/unset.js rename to src/utils/unset.js