diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage
new file mode 100644
index 0000000000000..d9ec1861c9979
--- /dev/null
+++ b/.ci/Jenkinsfile_coverage
@@ -0,0 +1,112 @@
+#!/bin/groovy
+
+library 'kibana-pipeline-library'
+kibanaLibrary.load() // load from the Jenkins instance
+
+stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a little bit
+ timeout(time: 180, unit: 'MINUTES') {
+ timestamps {
+ ansiColor('xterm') {
+ catchError {
+ withEnv([
+ 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc.
+ ]) {
+ parallel([
+ 'kibana-intake-agent': {
+ withEnv([
+ 'NODE_ENV=test' // Needed for jest tests only
+ ]) {
+ kibanaPipeline.legacyJobRunner('kibana-intake')()
+ }
+ },
+ 'x-pack-intake-agent': {
+ withEnv([
+ 'NODE_ENV=test' // Needed for jest tests only
+ ]) {
+ kibanaPipeline.legacyJobRunner('x-pack-intake')()
+ }
+ },
+ 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [
+ 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1),
+ 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2),
+ 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3),
+ 'oss-ciGroup4': kibanaPipeline.getOssCiGroupWorker(4),
+ 'oss-ciGroup5': kibanaPipeline.getOssCiGroupWorker(5),
+ 'oss-ciGroup6': kibanaPipeline.getOssCiGroupWorker(6),
+ 'oss-ciGroup7': kibanaPipeline.getOssCiGroupWorker(7),
+ 'oss-ciGroup8': kibanaPipeline.getOssCiGroupWorker(8),
+ 'oss-ciGroup9': kibanaPipeline.getOssCiGroupWorker(9),
+ 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10),
+ 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11),
+ 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12),
+ ]),
+ 'kibana-xpack-agent-1': kibanaPipeline.withWorkers('kibana-xpack-tests-1', { kibanaPipeline.buildXpack() }, [
+ 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1),
+ 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2),
+ ]),
+ 'kibana-xpack-agent-2': kibanaPipeline.withWorkers('kibana-xpack-tests-2', { kibanaPipeline.buildXpack() }, [
+ 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3),
+ 'xpack-ciGroup4': kibanaPipeline.getXpackCiGroupWorker(4),
+ ]),
+
+ 'kibana-xpack-agent-3': kibanaPipeline.withWorkers('kibana-xpack-tests-3', { kibanaPipeline.buildXpack() }, [
+ 'xpack-ciGroup5': kibanaPipeline.getXpackCiGroupWorker(5),
+ 'xpack-ciGroup6': kibanaPipeline.getXpackCiGroupWorker(6),
+ 'xpack-ciGroup7': kibanaPipeline.getXpackCiGroupWorker(7),
+ 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8),
+ 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9),
+ 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10),
+ ]),
+ ])
+ kibanaPipeline.jobRunner('tests-l', false) {
+ kibanaPipeline.downloadCoverageArtifacts()
+ kibanaPipeline.bash(
+ '''
+ # bootstrap from x-pack folder
+ source src/dev/ci_setup/setup_env.sh
+ cd x-pack
+ yarn kbn bootstrap --prefer-offline
+ cd ..
+ # extract archives
+ mkdir -p /tmp/extracted_coverage
+ echo extracting intakes
+ tar -xzf /tmp/downloaded_coverage/coverage/kibana-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage
+ tar -xzf /tmp/downloaded_coverage/coverage/x-pack-intake/kibana-coverage.tar.gz -C /tmp/extracted_coverage
+ echo extracting kibana-oss-tests
+ tar -xzf /tmp/downloaded_coverage/coverage/kibana-oss-tests/kibana-coverage.tar.gz -C /tmp/extracted_coverage
+ echo extracting kibana-xpack-tests
+ for i in {1..3}; do
+ tar -xzf /tmp/downloaded_coverage/coverage/kibana-xpack-tests-${i}/kibana-coverage.tar.gz -C /tmp/extracted_coverage
+ done
+ # replace path in json files to have valid html report
+ pwd=$(pwd)
+ du -sh /tmp/extracted_coverage/target/kibana-coverage/
+ echo replacing path in json files
+ for i in {1..9}; do
+ sed -i "s|/dev/shm/workspace/kibana|$pwd|g" /tmp/extracted_coverage/target/kibana-coverage/functional/${i}*.json &
+ done
+ wait
+ # merge oss & x-pack reports
+ echo merging coverage reports
+ yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/jest --report-dir target/kibana-coverage/jest-combined --reporter=html --reporter=json-summary
+ yarn nyc report --temp-dir /tmp/extracted_coverage/target/kibana-coverage/functional --report-dir target/kibana-coverage/functional-combined --reporter=html --reporter=json-summary
+ echo copy mocha reports
+ mkdir -p target/kibana-coverage/mocha-combined
+ cp -r /tmp/extracted_coverage/target/kibana-coverage/mocha target/kibana-coverage/mocha-combined
+ ''',
+ "run `yarn kbn bootstrap && merge coverage`"
+ )
+ sh 'tar -czf kibana-jest-coverage.tar.gz target/kibana-coverage/jest-combined/*'
+ kibanaPipeline.uploadCoverageArtifacts("coverage/jest-combined", 'kibana-jest-coverage.tar.gz')
+ sh 'tar -czf kibana-functional-coverage.tar.gz target/kibana-coverage/functional-combined/*'
+ kibanaPipeline.uploadCoverageArtifacts("coverage/functional-combined", 'kibana-functional-coverage.tar.gz')
+ sh 'tar -czf kibana-mocha-coverage.tar.gz target/kibana-coverage/mocha-combined/*'
+ kibanaPipeline.uploadCoverageArtifacts("coverage/mocha-combined", 'kibana-mocha-coverage.tar.gz')
+ }
+ }
+ }
+ kibanaPipeline.sendMail()
+ }
+ }
+ }
+}
diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky
index 669395564db44..f702405aad69e 100644
--- a/.ci/Jenkinsfile_flaky
+++ b/.ci/Jenkinsfile_flaky
@@ -99,7 +99,7 @@ def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWor
def numberOfWorkers = Math.min(numberOfExecutions, maxWorkerProcesses)
for(def i = 1; i <= numberOfWorkers; i++) {
- def workerExecutions = numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0)
+ def workerExecutions = floor(numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0))
workerMap["agent-${agentNumber}-worker-${i}"] = { workerNumber ->
for(def j = 0; j < workerExecutions; j++) {
diff --git a/.eslintrc.js b/.eslintrc.js
index 369aca347a2ec..47c543c22ba08 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -82,37 +82,18 @@ module.exports = {
'react-hooks/exhaustive-deps': 'off',
},
},
- {
- files: ['src/legacy/core_plugins/kibana/**/*.{js,ts,tsx}'],
- rules: {
- 'react-hooks/rules-of-hooks': 'off',
- 'react-hooks/exhaustive-deps': 'off',
- },
- },
{
files: ['src/legacy/core_plugins/tile_map/**/*.{js,ts,tsx}'],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
- {
- files: ['src/legacy/core_plugins/vis_type_metric/**/*.{js,ts,tsx}'],
- rules: {
- 'jsx-a11y/click-events-have-key-events': 'off',
- },
- },
{
files: ['src/legacy/core_plugins/vis_type_table/**/*.{js,ts,tsx}'],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
- {
- files: ['src/legacy/core_plugins/vis_type_vega/**/*.{js,ts,tsx}'],
- rules: {
- 'react-hooks/exhaustive-deps': 'off',
- },
- },
{
files: ['src/legacy/ui/public/vis/**/*.{js,ts,tsx}'],
rules: {
@@ -241,6 +222,7 @@ module.exports = {
'!x-pack/test/**/*',
'(src|x-pack)/plugins/**/(public|server)/**/*',
'src/core/(public|server)/**/*',
+ 'examples/**/*',
],
from: [
'src/core/public/**/*',
@@ -277,11 +259,15 @@ module.exports = {
'x-pack/legacy/plugins/**/*',
'!x-pack/legacy/plugins/*/server/**/*',
'!x-pack/legacy/plugins/*/index.{js,ts,tsx}',
+
+ 'examples/**/*',
+ '!examples/**/server/**/*',
],
from: [
'src/core/server',
'src/core/server/**/*',
'(src|x-pack)/plugins/*/server/**/*',
+ 'examples/**/server/**/*',
],
errorMessage:
'Server modules cannot be imported into client modules or shared modules.',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 1137fb99f81a7..a0a22446ba31d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -14,6 +14,7 @@
/src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/home/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app
+/src/legacy/core_plugins/metrics/ @elastic/kibana-app
/src/plugins/home/ @elastic/kibana-app
/src/plugins/kibana_legacy/ @elastic/kibana-app
/src/plugins/timelion/ @elastic/kibana-app
@@ -30,6 +31,7 @@
/src/plugins/visualizations/ @elastic/kibana-app-arch
/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch
/src/legacy/core_plugins/data/ @elastic/kibana-app-arch
+/src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch
/src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch
/src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch
@@ -146,6 +148,3 @@
/x-pack/legacy/plugins/searchprofiler/ @elastic/es-ui
/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/legacy/plugins/watcher/ @elastic/es-ui
-
-# Kibana TSVB external contractors
-/src/legacy/core_plugins/metrics/ @elastic/kibana-tsvb-external
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 06e08c85dafec..6ae3db559b61b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -190,6 +190,19 @@ These snapshots are built on a nightly basis which expire after a couple weeks.
yarn es snapshot
```
+##### Keeping data between snapshots
+
+If you want to keep the data inside your Elasticsearch between usages of this command,
+you should use the following command, to keep your data folder outside the downloaded snapshot
+folder:
+
+```bash
+yarn es snapshot -E path.data=../data
+```
+
+The same parameter can be used with the source and archive command shown in the following
+paragraphs.
+
#### Source
By default, it will reference an [elasticsearch](https://github.com/elastic/elasticsearch) checkout which is a sibling to the Kibana directory named `elasticsearch`. If you wish to use a checkout in another location you can provide that by supplying `--source-path`
diff --git a/docs/apm/settings.asciidoc b/docs/apm/settings.asciidoc
index 2fc8748f13b09..37122fc9c635d 100644
--- a/docs/apm/settings.asciidoc
+++ b/docs/apm/settings.asciidoc
@@ -3,8 +3,16 @@
[[apm-settings-in-kibana]]
=== APM settings in Kibana
-You do not need to configure any settings to use APM. It is enabled by default.
-If you'd like to change any of the default values,
-copy and paste the relevant settings below into your `kibana.yml` configuration file.
+You do not need to configure any settings to use the APM app. It is enabled by default.
+
+[float]
+[[apm-indices-settings]]
+==== APM Indices
+
+include::./../settings/apm-settings.asciidoc[tag=apm-indices-settings]
+
+[float]
+[[general-apm-settings]]
+==== General APM settings
include::./../settings/apm-settings.asciidoc[tag=general-apm-settings]
diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc
index ec0863b09d653..22279b69b70fe 100644
--- a/docs/apm/troubleshooting.asciidoc
+++ b/docs/apm/troubleshooting.asciidoc
@@ -17,6 +17,7 @@ This section can help with any of the following:
There are a number of factors that could be at play here.
One important thing to double-check first is your index template.
+*Index template*
An APM index template must exist for the APM app to work correctly.
By default, this index template is created by APM Server on startup.
However, this only happens if `setup.template.enabled` is `true` in `apm-server.yml`.
@@ -34,14 +35,21 @@ GET /_template/apm-{version}
--------------------------------------------------
// CONSOLE
+*Using Logstash, Kafka, etc.*
If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka),
then the index template will not be set up automatically. Instead, you'll need to
-{apm-server-ref}/_manually_loading_template_configuration.html#load-template-manually-alternate[load the template manually].
+{apm-server-ref}/_manually_loading_template_configuration.html[load the template manually].
-Finally, this problem can also occur if you've changed the index name that you write APM data to.
-The default index pattern can be found {apm-server-ref}/elasticsearch-output.html#index-option-es[here].
-If you change this setting, you must also configure the `setup.template.name` and `setup.template.pattern` options.
+*Using a custom index names*
+This problem can also occur if you've customized the index name that you write APM data to.
+The default index name that APM writes events to can be found
+{apm-server-ref}/elasticsearch-output.html#index-option-es[here].
+If you change the default, you must also configure the `setup.template.name` and `setup.template.pattern` options.
See {apm-server-ref}/configuration-template.html[Load the Elasticsearch index template].
+If the Elasticsearch index template has already been successfully loaded to the index,
+you can customize the indices that the APM app uses to display data.
+Navigate to *APM* > *Settings* > *Indices*, and change all `apm_oss.*Pattern` values to
+include the new index pattern. For example: `customIndexName-*`.
==== Unknown route
diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md
index 1ce18834f5319..a4fa3f17d0d94 100644
--- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md
+++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md
@@ -9,5 +9,5 @@ Search for objects
Signature:
```typescript
-find: (options: Pick) => Promise>;
+find: (options: Pick) => Promise>;
```
diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md
index 3b916db972673..88485aa71f7c5 100644
--- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md
+++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md
@@ -24,7 +24,7 @@ The constructor for this class is marked as internal. Third-party code should no
| [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: { id: string; type: string; }[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id |
| [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object |
| [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object |
-| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects |
+| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects |
| [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object |
## Methods
diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md
index ed7d028a1ec8a..bb1f481c9ef4f 100644
--- a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md
+++ b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md
@@ -9,14 +9,14 @@ Creates an instance of [IScopedClusterClient](./kibana-plugin-server.iscopedclus
Signature:
```typescript
-asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): IScopedClusterClient;
+asScoped(request?: ScopeableRequest): IScopedClusterClient;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| request | KibanaRequest | LegacyRequest | FakeRequest | Request the IScopedClusterClient instance will be scoped to. Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform |
+| request | ScopeableRequest | Request the IScopedClusterClient instance will be scoped to. Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform |
Returns:
diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.md b/docs/development/core/server/kibana-plugin-server.clusterclient.md
index 5fdda7ef3e499..d547b846e65b7 100644
--- a/docs/development/core/server/kibana-plugin-server.clusterclient.md
+++ b/docs/development/core/server/kibana-plugin-server.clusterclient.md
@@ -4,7 +4,7 @@
## ClusterClient class
-Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`).
+Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`).
See [ClusterClient](./kibana-plugin-server.clusterclient.md).
diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient.md
new file mode 100644
index 0000000000000..415423f555266
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient](./kibana-plugin-server.elasticsearchservicesetup.adminclient.md)
+
+## ElasticsearchServiceSetup.adminClient property
+
+A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md).
+
+Signature:
+
+```typescript
+readonly adminClient: IClusterClient;
+```
+
+## Example
+
+
+```js
+const client = core.elasticsearch.adminClient;
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md
deleted file mode 100644
index b5bfc68d3ca0c..0000000000000
--- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient_.md)
-
-## ElasticsearchServiceSetup.adminClient$ property
-
-Observable of clients for the `admin` cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md).
-
-
-```js
-const client = await elasticsearch.adminClient$.pipe(take(1)).toPromise();
-
-```
-
-Signature:
-
-```typescript
-readonly adminClient$: Observable;
-```
diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md
index 3d26f2d4cec88..797f402cc2580 100644
--- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md
+++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md
@@ -9,7 +9,7 @@ Create application specific Elasticsearch cluster API client with customized con
Signature:
```typescript
-readonly createClient: (type: string, clientConfig?: Partial) => IClusterClient;
+readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient;
```
## Example
diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient.md
new file mode 100644
index 0000000000000..e9845dce6915d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient](./kibana-plugin-server.elasticsearchservicesetup.dataclient.md)
+
+## ElasticsearchServiceSetup.dataClient property
+
+A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md).
+
+Signature:
+
+```typescript
+readonly dataClient: IClusterClient;
+```
+
+## Example
+
+
+```js
+const client = core.elasticsearch.dataClient;
+
+```
+
diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md
deleted file mode 100644
index 9411f2f6b8694..0000000000000
--- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) > [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient_.md)
-
-## ElasticsearchServiceSetup.dataClient$ property
-
-Observable of clients for the `data` cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md).
-
-
-```js
-const client = await elasticsearch.dataClient$.pipe(take(1)).toPromise();
-
-```
-
-Signature:
-
-```typescript
-readonly dataClient$: Observable;
-```
diff --git a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md
index e3d151cdc0d8b..2de3f6e6d1bbc 100644
--- a/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md
+++ b/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.md
@@ -15,17 +15,7 @@ export interface ElasticsearchServiceSetup
| Property | Type | Description |
| --- | --- | --- |
-| [adminClient$](./kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Observable<IClusterClient> | Observable of clients for the admin cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md).
-```js
-const client = await elasticsearch.adminClient$.pipe(take(1)).toPromise();
-
-```
- |
-| [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => IClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). |
-| [dataClient$](./kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Observable<IClusterClient> | Observable of clients for the data cluster. Observable emits when Elasticsearch config changes on the Kibana server. See [IClusterClient](./kibana-plugin-server.iclusterclient.md).
-```js
-const client = await elasticsearch.dataClient$.pipe(take(1)).toPromise();
-
-```
- |
+| [adminClient](./kibana-plugin-server.elasticsearchservicesetup.adminclient.md) | IClusterClient | A client for the admin cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). |
+| [createClient](./kibana-plugin-server.elasticsearchservicesetup.createclient.md) | (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ICustomClusterClient | Create application specific Elasticsearch cluster API client with customized config. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). |
+| [dataClient](./kibana-plugin-server.elasticsearchservicesetup.dataclient.md) | IClusterClient | A client for the data cluster. All Elasticsearch config value changes are processed under the hood. See [IClusterClient](./kibana-plugin-server.iclusterclient.md). |
diff --git a/docs/development/core/server/kibana-plugin-server.iclusterclient.md b/docs/development/core/server/kibana-plugin-server.iclusterclient.md
index 834afa6db5157..e7435a9d91a74 100644
--- a/docs/development/core/server/kibana-plugin-server.iclusterclient.md
+++ b/docs/development/core/server/kibana-plugin-server.iclusterclient.md
@@ -4,12 +4,12 @@
## IClusterClient type
-Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`).
+Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`).
See [ClusterClient](./kibana-plugin-server.clusterclient.md).
Signature:
```typescript
-export declare type IClusterClient = Pick;
+export declare type IClusterClient = Pick;
```
diff --git a/docs/development/core/server/kibana-plugin-server.icustomclusterclient.md b/docs/development/core/server/kibana-plugin-server.icustomclusterclient.md
new file mode 100644
index 0000000000000..d7511a119fc0f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.icustomclusterclient.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ICustomClusterClient](./kibana-plugin-server.icustomclusterclient.md)
+
+## ICustomClusterClient type
+
+Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via `asScoped(...)`).
+
+See [ClusterClient](./kibana-plugin-server.clusterclient.md).
+
+Signature:
+
+```typescript
+export declare type ICustomClusterClient = Pick;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md
index 85874f5ec16ba..2e496aa0c46fc 100644
--- a/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md
+++ b/docs/development/core/server/kibana-plugin-server.kibanaresponsefactory.md
@@ -10,7 +10,7 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom
```typescript
kibanaResponseFactory: {
- custom: | Buffer | Stream | {
+ custom: | {
message: string | Error;
attributes?: Record | undefined;
} | undefined>(options: CustomHttpResponseOptions) => KibanaResponse;
@@ -21,9 +21,9 @@ kibanaResponseFactory: {
conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse;
internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse;
customError: (options: CustomHttpResponseOptions) => KibanaResponse;
- redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>;
- ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>;
- accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>;
+ redirected: (options: RedirectResponseOptions) => KibanaResponse>;
+ ok: (options?: HttpResponseOptions) => KibanaResponse>;
+ accepted: (options?: HttpResponseOptions) => KibanaResponse>;
noContent: (options?: HttpResponseOptions) => KibanaResponse;
}
```
diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index 5e7f84c55244d..5e28643843af3 100644
--- a/docs/development/core/server/kibana-plugin-server.md
+++ b/docs/development/core/server/kibana-plugin-server.md
@@ -17,7 +17,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| Class | Description |
| --- | --- |
| [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path |
-| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). |
+| [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). |
| [CspConfig](./kibana-plugin-server.cspconfig.md) | CSP configuration for use in Kibana. |
| [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] |
| [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. |
@@ -175,8 +175,9 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [Headers](./kibana-plugin-server.headers.md) | Http request headers to read. |
| [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) | Data send to the client as a response payload. |
| [IBasePath](./kibana-plugin-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-server.basepath.md) |
-| [IClusterClient](./kibana-plugin-server.iclusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). |
+| [IClusterClient](./kibana-plugin-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). |
| [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
+| [ICustomClusterClient](./kibana-plugin-server.icustomclusterclient.md) | Represents an Elasticsearch cluster API client created by a plugin. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). |
| [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. |
| [ISavedObjectsRepository](./kibana-plugin-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) |
| [IScopedClusterClient](./kibana-plugin-server.iscopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). |
@@ -213,6 +214,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) |
| [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. |
| [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. |
+| [ScopeableRequest](./kibana-plugin-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-server.kibanarequest.md). |
| [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) | |
| [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | UI element type to represent the settings. |
diff --git a/docs/development/core/server/kibana-plugin-server.scopeablerequest.md b/docs/development/core/server/kibana-plugin-server.scopeablerequest.md
new file mode 100644
index 0000000000000..5a9443376996d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.scopeablerequest.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ScopeableRequest](./kibana-plugin-server.scopeablerequest.md)
+
+## ScopeableRequest type
+
+A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.
+
+See [KibanaRequest](./kibana-plugin-server.kibanarequest.md).
+
+Signature:
+
+```typescript
+export declare type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest;
+```
diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc
index bbe4b3b68e03b..0ad27e68f7fe9 100644
--- a/docs/management/managing-licenses.asciidoc
+++ b/docs/management/managing-licenses.asciidoc
@@ -1,28 +1,185 @@
[[managing-licenses]]
-== License Management
+== License management
-When you install {kib}, it generates a Basic license
-with no expiration date. Go to *Management > License Management* to view the
-status of your license, start a 30-day trial, or install a new license.
+When you install the default distribution of {kib}, you receive a basic license
+with no expiration date. For the full list of free features that are included in
+the basic license, see https://www.elastic.co/subscriptions[the subscription page].
-To learn more about the available license levels,
-see https://www.elastic.co/subscriptions[the subscription page].
+If you want to try out the full set of platinum features, you can activate a
+30-day trial license. Go to *Management > License Management* to view the
+status of your license, start a trial, or install a new license.
-You can activate a 30-day trial license to try out the full set of
-https://www.elastic.co/subscriptions[Platinum features], including machine learning,
-advanced security, alerting, graph capabilities, and more.
+NOTE: You can start a trial only if your cluster has not already activated a
+trial license for the current major product version. For example, if you have
+already activated a trial for v6.0, you cannot start a new trial until
+v7.0. You can, however, contact `info@elastic.co` to request an extended trial
+license.
-When you activate a new license level, new features will appear in the left sidebar
+When you activate a new license level, new features appear in the left sidebar
of the *Management* page.
[role="screenshot"]
image::images/management-license.png[]
-At the end of the trial period, the Platinum features operate in a
-{stack-ov}/license-expiration.html[degraded mode]. You can revert to a Basic
-license, extend the trial, or purchase a subscription.
+At the end of the trial period, the platinum features operate in a
+<>. You can revert to a basic license,
+extend the trial, or purchase a subscription.
+TIP: If {security-features} are enabled, before you revert to a basic license or
+install a gold or platinum license, you must configure Transport Layer Security
+(TLS) in {es}. See {ref}/encrypting-communications.html[Encrypting communications].
+{kib} and the {ref}/start-basic.html[start basic API] provide a list of all of
+the features that will no longer be supported if you revert to a basic license.
-TIP: If {security-features} are enabled, before you revert to a Basic license or install
-a Gold or Platinum license, you must configure Transport Layer Security (TLS) in {es}.
-See {ref}/encrypting-communications.html[Encrypting communications].
\ No newline at end of file
+[discrete]
+[[update-license]]
+=== Update your license
+
+You can update your license at runtime without shutting down your {es} nodes.
+License updates take effect immediately. The license is provided as a _JSON_
+file that you install in {kib} or by using the
+{ref}/update-license.html[update license API].
+
+TIP: If you are using a basic or trial license, {security-features} are disabled
+by default. In all other licenses, {security-features} are enabled by default;
+you must secure the {stack} or disable the {security-features}.
+
+[discrete]
+[[license-expiration]]
+=== License expiration
+
+Your license is time based and expires at a future date. If you're using
+{monitor-features} and your license will expire within 30 days, a license
+expiration warning is displayed prominently. Warnings are also displayed on
+startup and written to the {es} log starting 30 days from the expiration date.
+These error messages tell you when the license expires and what features will be
+disabled if you do not update the license.
+
+IMPORTANT: You should update your license as soon as possible. You are
+essentially flying blind when running with an expired license. Access to the
+cluster health and stats APIs is critical for monitoring and managing an {es}
+cluster.
+
+[discrete]
+[[expiration-beats]]
+==== Beats
+
+* Beats will continue to poll centrally-managed configuration.
+
+[discrete]
+[[expiration-elasticsearch]]
+==== {es}
+
+// Upgrade API is disabled
+* The deprecation API is disabled.
+* SQL support is disabled.
+* Aggregations provided by the analytics plugin are no longer usable.
+
+[discrete]
+[[expiration-watcher]]
+==== {stack} {alert-features}
+
+* The PUT and GET watch APIs are disabled. The DELETE watch API continues to work.
+* Watches execute and write to the history.
+* The actions of the watches do not execute.
+
+[discrete]
+[[expiration-graph]]
+==== {stack} {graph-features}
+
+* Graph explore APIs are disabled.
+
+[discrete]
+[[expiration-ml]]
+==== {stack} {ml-features}
+
+* APIs to create {anomaly-jobs}, open jobs, send data to jobs, create {dfeeds},
+and start {dfeeds} are disabled.
+* All started {dfeeds} are stopped.
+* All open {anomaly-jobs} are closed.
+* APIs to create and start {dfanalytics-jobs} are disabled.
+* Existing {anomaly-job} and {dfanalytics-job} results continue to be available
+by using {kib} or APIs.
+
+[discrete]
+[[expiration-monitoring]]
+==== {stack} {monitor-features}
+
+* The agent stops collecting cluster and indices metrics.
+* The agent stops automatically cleaning indices older than
+`xpack.monitoring.history.duration`.
+
+[discrete]
+[[expiration-security]]
+==== {stack} {security-features}
+
+* Cluster health, cluster stats, and indices stats operations are blocked.
+* All data operations (read and write) continue to work.
+
+Once the license expires, calls to the cluster health, cluster stats, and index
+stats APIs fail with a `security_exception` and return a 403 HTTP status code.
+
+[source,sh]
+-----------------------------------------------------
+{
+ "error": {
+ "root_cause": [
+ {
+ "type": "security_exception",
+ "reason": "current license is non-compliant for [security]",
+ "license.expired.feature": "security"
+ }
+ ],
+ "type": "security_exception",
+ "reason": "current license is non-compliant for [security]",
+ "license.expired.feature": "security"
+ },
+ "status": 403
+}
+-----------------------------------------------------
+
+This message enables automatic monitoring systems to easily detect the license
+failure without immediately impacting other users.
+
+[discrete]
+[[expiration-logstash]]
+==== {ls} pipeline management
+
+* Cannot create new pipelines or edit or delete existing pipelines from the UI.
+* Cannot list or view existing pipelines from the UI.
+* Cannot run Logstash instances which are registered to listen to existing pipelines.
+//TBD: * Logstash will continue to poll centrally-managed pipelines
+
+[discrete]
+[[expiration-kibana]]
+==== {kib}
+
+* Users can still log into {kib}.
+* {kib} works for data exploration and visualization, but some features
+are disabled.
+* The license management UI is available to easily upgrade your license. See
+<> and <>.
+
+[discrete]
+[[expiration-reporting]]
+==== {kib} {report-features}
+
+* Reporting is no longer available in {kib}.
+* Report generation URLs stop working.
+* Existing reports are no longer accessible.
+
+[discrete]
+[[expiration-rollups]]
+==== {rollups-cap}
+
+* {rollup-jobs-cap} cannot be created or started.
+* Existing {rollup-jobs} can be stopped and deleted.
+* The get rollup caps and rollup search APIs continue to function.
+
+[discrete]
+[[expiration-transforms]]
+==== {transforms-cap}
+
+* {transforms-cap} cannot be created, previewed, started, or updated.
+* Existing {transforms} can be stopped and deleted.
+* Existing {transform} results continue to be available.
diff --git a/docs/management/snapshot-restore/index.asciidoc b/docs/management/snapshot-restore/index.asciidoc
index f19aaa122675e..dc722c24af76c 100644
--- a/docs/management/snapshot-restore/index.asciidoc
+++ b/docs/management/snapshot-restore/index.asciidoc
@@ -21,7 +21,7 @@ With this UI, you can:
image:management/snapshot-restore/images/snapshot_list.png["Snapshot list"]
Before using this feature, you should be familiar with how snapshots work.
-{ref}/modules-snapshots.html[Snapshot and Restore] is a good source for
+{ref}/snapshot-restore.html[Snapshot and Restore] is a good source for
more detailed information.
[float]
@@ -35,9 +35,9 @@ registering one.
{kib} supports three repository types
out of the box: shared file system, read-only URL, and source-only.
For more information on these repositories and their settings,
-see {ref}/modules-snapshots.html#snapshots-repositories[Repositories].
+see {ref}/snapshots-register-repository.html[Repositories].
To use other repositories, such as S3, see
-{ref}/modules-snapshots.html#_repository_plugins[Repository plugins].
+{ref}/snapshots-register-repository.html#snapshots-repository-plugins[Repository plugins].
Once you create a repository, it is listed in the *Repositories*
@@ -61,7 +61,7 @@ into each snapshot for further investigation.
image:management/snapshot-restore/images/snapshot_details.png["Snapshot details"]
If you don’t have any snapshots, you can create them from the {kib} <>. The
-{ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API]
+{ref}/snapshots-take-snapshot.html[snapshot API]
takes the current state and data in your index or cluster, and then saves it to a
shared repository.
@@ -162,7 +162,7 @@ Ready to try *Snapshot and Restore*? In this tutorial, you'll learn to:
This example shows you how to register a shared file system repository
and store snapshots.
Before you begin, you must register the location of the repository in the
-{ref}/modules-snapshots.html#_shared_file_system_repository[path.repo] setting on
+{ref}/snapshots-register-repository.html#snapshots-filesystem-repository[path.repo] setting on
your master and data nodes. You can do this in one of two ways:
* Edit your `elasticsearch.yml` to include the `path.repo` setting.
@@ -197,7 +197,7 @@ The repository currently doesn’t have any snapshots.
[float]
==== Add a snapshot to the repository
-Use the {ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API] to create a snapshot.
+Use the {ref}/snapshots-take-snapshot.html[snapshot API] to create a snapshot.
. Go to *Dev Tools > Console*.
. Create the snapshot:
@@ -206,7 +206,7 @@ Use the {ref}//modules-snapshots.html#snapshots-take-snapshot[snapshot API] to c
PUT /_snapshot/my_backup/2019-04-25_snapshot?wait_for_completion=true
+
In this example, the snapshot name is `2019-04-25_snapshot`. You can also
-use {ref}//date-math-index-names.html[date math expression] for the snapshot name.
+use {ref}/date-math-index-names.html[date math expression] for the snapshot name.
+
[role="screenshot"]
image:management/snapshot-restore/images/create_snapshot.png["Create snapshot"]
diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc
index 8d28b55a6502f..a6eeffec51cb0 100644
--- a/docs/settings/apm-settings.asciidoc
+++ b/docs/settings/apm-settings.asciidoc
@@ -5,9 +5,23 @@
APM settings
++++
-You do not need to configure any settings to use APM. It is enabled by default.
-If you'd like to change any of the default values,
-copy and paste the relevant settings below into your `kibana.yml` configuration file.
+You do not need to configure any settings to use the APM app. It is enabled by default.
+
+[float]
+[[apm-indices-settings-kb]]
+==== APM Indices
+
+// This content is reused in the APM app documentation.
+// Any changes made in this file will be seen there as well.
+// tag::apm-indices-settings[]
+
+Index defaults can be changed in Kibana. Navigate to *APM* > *Settings* > *Indices*.
+Index settings in the APM app take precedence over those set in `kibana.yml`.
+
+[role="screenshot"]
+image::settings/images/apm-settings.png[APM app settings in Kibana]
+
+// end::apm-indices-settings[]
[float]
[[general-apm-settings-kb]]
@@ -17,6 +31,9 @@ copy and paste the relevant settings below into your `kibana.yml` configuration
// Any changes made in this file will be seen there as well.
// tag::general-apm-settings[]
+If you'd like to change any of the default values,
+copy and paste the relevant settings below into your `kibana.yml` configuration file.
+
xpack.apm.enabled:: Set to `false` to disabled the APM plugin {kib}. Defaults to
`true`.
diff --git a/docs/settings/images/apm-settings.png b/docs/settings/images/apm-settings.png
new file mode 100644
index 0000000000000..876f135da9356
Binary files /dev/null and b/docs/settings/images/apm-settings.png differ
diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc
index 2fc74d2ffee32..38a46a3cde5a0 100644
--- a/docs/settings/monitoring-settings.asciidoc
+++ b/docs/settings/monitoring-settings.asciidoc
@@ -66,13 +66,6 @@ both the {es} monitoring cluster and the {es} production cluster.
If not set, {kib} uses the value of the `elasticsearch.password` setting.
-`telemetry.enabled`::
-Set to `true` (default) to send cluster statistics to Elastic. Reporting your
-cluster statistics helps us improve your user experience. Your data is never
-shared with anyone. Set to `false` to disable statistics reporting from any
-browser connected to the {kib} instance. You can also opt out through the
-*Advanced Settings* in {kib}.
-
`xpack.monitoring.elasticsearch.pingTimeout`::
Specifies the time in milliseconds to wait for {es} to respond to internal
health checks. By default, it matches the `elasticsearch.pingTimeout` setting,
diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc
index a754f91e9f22a..a9fa2bd18d315 100644
--- a/docs/settings/reporting-settings.asciidoc
+++ b/docs/settings/reporting-settings.asciidoc
@@ -54,6 +54,14 @@ The protocol for accessing Kibana, typically `http` or `https`.
`xpack.reporting.kibanaServer.hostname`::
The hostname for accessing {kib}, if different from the `server.host` value.
+[NOTE]
+============
+Reporting authenticates requests on the Kibana page only when the hostname matches the
+`xpack.reporting.kibanaServer.hostname` setting. Therefore Reporting would fail if the
+set value redirects to another server. For that reason, `"0"` is an invalid setting
+because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`.
+============
+
[float]
[[reporting-job-queue-settings]]
diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc
index d6dd4378da1b7..16d68a7759f77 100644
--- a/docs/settings/security-settings.asciidoc
+++ b/docs/settings/security-settings.asciidoc
@@ -45,7 +45,7 @@ if this setting isn't the same for all instances of {kib}.
`xpack.security.secureCookies`::
Sets the `secure` flag of the session cookie. The default value is `false`. It
-is set to `true` if `server.ssl.certificate` and `server.ssl.key` are set. Set
+is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set
this to `true` if SSL is configured outside of {kib} (for example, you are
routing requests through a load balancer or proxy).
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index 01e6bd51ea50b..535ad16978217 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -82,31 +82,52 @@ Elasticsearch nodes on startup.
`elasticsearch.sniffOnConnectionFault:`:: *Default: false* Update the list of
Elasticsearch nodes immediately following a connection fault.
-`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls
-whether to always present the certificate specified by
-`elasticsearch.ssl.certificate` when requested. This applies to all requests to
-Elasticsearch, including requests that are proxied for end-users. Setting this
-to `true` when Elasticsearch is using certificates to authenticate users can
-lead to proxied requests for end-users being executed as the identity tied to
-the configured certificate.
-
-`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Optional
-settings that provide the paths to the PEM-format SSL certificate and key files.
-These files are used to verify the identity of Kibana to Elasticsearch and are
-required when `xpack.security.http.ssl.client_authentication` in Elasticsearch is
-set to `required`.
-
-`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.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.verificationMode:`:: *Default: full* Controls the
-verification of certificates presented by Elasticsearch. Valid values are `none`,
-`certificate`, and `full`. `full` performs hostname verification, and
-`certificate` does not.
+`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls whether to always present the certificate specified by
+`elasticsearch.ssl.certificate` or `elasticsearch.ssl.keystore.path` when requested. This setting applies to all requests to Elasticsearch,
+including requests that are proxied for end users. Setting this to `true` when Elasticsearch is using certificates to authenticate users can
+lead to proxied requests for end users being executed as the identity tied to the configured certificate.
+
+`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively.
+When `xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required` or `optional`, the certificate and key are used
+to prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster.
++
+--
+NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`.
+--
+
+`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. These certificates may consist of a root
+certificate authority (CA), and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is used to
+establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may
+be specified via `elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`.
+
+`elasticsearch.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via
+`elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted.
+
+`elasticsearch.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. When
+`xpack.security.http.ssl.client_authentication` in Elasticsearch is set to `required` or `optional`, the certificate and key are used to
+prove Kibana's identity when it makes an outbound request to your Elasticsearch cluster. If the file contains any additional certificates,
+those will be used as a trusted certificate chain for your Elasticsearch cluster. This chain is used to establish trust when Kibana creates
+an SSL connection with your Elasticsearch cluster. In addition to this setting, trusted certificates may be specified via
+`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`.
++
+--
+NOTE: This setting cannot be used in conjunction with `elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`.
+--
+
+`elasticsearch.ssl.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has
+no password, leave this unset. If your key store has an empty password, set this to `""`.
+
+`elasticsearch.ssl.truststore.path:`:: Path to a PKCS #12 trust store that contains one or more X.509 certificates. This may consist of a
+root certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for your Elasticsearch cluster.
+This chain is used to establish trust when Kibana creates an SSL connection with your Elasticsearch cluster. In addition to this setting,
+trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`.
+
+`elasticsearch.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password,
+leave this unset. If your trust store has an empty password, set this to `""`.
+
+`elasticsearch.ssl.verificationMode:`:: *Default: full* Controls the verification of certificates presented by Elasticsearch. Valid values
+are `none`, `certificate`, and `full`. `full` performs hostname verification and `certificate` does not. This setting is used only when
+traffic to Elasticsearch is encrypted, which is specified by using the HTTPS protocol in `elasticsearch.hosts`.
`elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait
for Elasticsearch at Kibana startup before retrying.
@@ -325,11 +346,19 @@ default is `true`.
`server.socketTimeout:`:: *Default: "120000"* The number of milliseconds to wait before closing an
inactive socket.
-`server.ssl.certificate:` and `server.ssl.key:`:: Paths to the PEM-format SSL
-certificate and SSL key files, respectively.
+`server.ssl.certificate:` and `server.ssl.key:`:: Paths to a PEM-encoded X.509 certificate and its private key, respectively. These are used
+when enabling SSL for inbound requests from web browsers to the Kibana server.
++
+--
+NOTE: These settings cannot be used in conjunction with `server.ssl.keystore.path`.
+--
-`server.ssl.certificateAuthorities:`:: List of paths to PEM encoded certificate
-files that should be trusted.
+`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificates. These certificates may consist of a root
+certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is used when a
+web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the end-entity
+certificate to establish trust. This chain is also used to determine whether client certificates should be trusted when PKI authentication
+is enabled. In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or
+`server.ssl.truststore.path`.
`server.ssl.cipherSuites:`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*.
Details on the format, and the valid options, are available via the
@@ -339,12 +368,36 @@ https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenS
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.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.enabled:`:: *Default: "false"* Enables SSL for inbound requests from the browser to the Kibana server. When set to `true`, a
+certificate and private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of
+`server.ssl.certificate` and `server.ssl.key`.
+
+`server.ssl.keyPassphrase:`:: The passphrase that will be used to decrypt the private key that is specified via `server.ssl.key`. This value
+is optional, as the key may not be encrypted.
+
+`server.ssl.keystore.path:`:: Path to a PKCS #12 file that contains an X.509 certificate with its private key. These are used when enabling
+SSL for inbound requests from web browsers to the Kibana server. If the file contains any additional certificates, those will be used as a
+trusted certificate chain for Kibana. This chain is used when a web browser creates an SSL connection with the Kibana server; the
+certificate chain is sent to the browser along with the end-entity certificate to establish trust. This chain is also used to determine
+whether client certificates should be trusted when PKI authentication is enabled. In addition to this setting, trusted certificates may be
+specified via `server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`.
++
+--
+NOTE: This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key`.
+--
-`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.keystore.password:`:: The password that will be used to decrypt the key store and its private key. If your key store has no
+password, leave this unset. If your key store has an empty password, set this to `""`.
+
+`server.ssl.truststore.path:`:: Path to a PKCS #12 trust store that contains one or more X.509 certificates. These certificates may consist
+of a root certificate authority (CA) and one or more intermediate CAs, which make up a trusted certificate chain for Kibana. This chain is
+used when a web browser creates an SSL connection with the Kibana server; the certificate chain is sent to the browser along with the
+end-entity certificate to establish trust. This chain is also used to determine whether client certificates should be trusted when PKI
+authentication is enabled. In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or
+`server.ssl.keystore.path`.
+
+`server.ssl.truststore.password:`:: The password that will be used to decrypt the trust store. If your trust store has no password, leave
+this unset. If your trust store has an empty password, set this to `""`.
`server.ssl.redirectHttpFromPort:`:: Kibana will bind to this port and redirect
all http requests to https over the port configured as `server.port`.
@@ -380,6 +433,11 @@ cannot be `false` at the same time.
To enable telemetry and prevent users from disabling it,
set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`.
+`telemetry.enabled`:: *Default: true* Reporting your cluster statistics helps
+us improve your user experience. Your data is never shared with anyone. Set to
+`false` to disable telemetry capabilities entirely. You can alternatively opt
+out through the *Advanced Settings* in {kib}.
+
`vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch.
`xpack.license_management.enabled`:: *Default: true* Set this value to false to
diff --git a/docs/spaces/images/spaces-configure-landing-page.png b/docs/spaces/images/spaces-configure-landing-page.png
new file mode 100644
index 0000000000000..15006594b6d7b
Binary files /dev/null and b/docs/spaces/images/spaces-configure-landing-page.png differ
diff --git a/docs/spaces/index.asciidoc b/docs/spaces/index.asciidoc
index 69655aac521e7..fb5ef670692dc 100644
--- a/docs/spaces/index.asciidoc
+++ b/docs/spaces/index.asciidoc
@@ -2,13 +2,13 @@
[[xpack-spaces]]
== Spaces
-Spaces enable you to organize your dashboards and other saved
-objects into meaningful categories. Once inside a space, you see only
-the dashboards and saved objects that belong to that space.
+Spaces enable you to organize your dashboards and other saved
+objects into meaningful categories. Once inside a space, you see only
+the dashboards and saved objects that belong to that space.
-{kib} creates a default space for you.
-After you create your own
-spaces, you're asked to choose a space when you log in to Kibana. You can change your
+{kib} creates a default space for you.
+After you create your own
+spaces, you're asked to choose a space when you log in to Kibana. You can change your
current space at any time by using the menu in the upper left.
[role="screenshot"]
@@ -29,24 +29,24 @@ Kibana supports spaces in several ways. You can:
[[spaces-managing]]
=== View, create, and delete spaces
-Go to **Management > Spaces** for an overview of your spaces. This view provides actions
+Go to **Management > Spaces** for an overview of your spaces. This view provides actions
for you to create, edit, and delete spaces.
[role="screenshot"]
image::spaces/images/space-management.png["Space management"]
[float]
-==== Create or edit a space
+==== Create or edit a space
-You can create as many spaces as you like. Click *Create a space* and provide a name,
-URL identifier, optional description.
+You can create as many spaces as you like. Click *Create a space* and provide a name,
+URL identifier, optional description.
-The URL identifier is a short text string that becomes part of the
-{kib} URL when you are inside that space. {kib} suggests a URL identifier based
+The URL identifier is a short text string that becomes part of the
+{kib} URL when you are inside that space. {kib} suggests a URL identifier based
on the name of your space, but you can customize the identifier to your liking.
You cannot change the space identifier once you create the space.
-{kib} also has an <>
+{kib} also has an <>
if you prefer to create spaces programatically.
[role="screenshot"]
@@ -55,7 +55,7 @@ image::spaces/images/edit-space.png["Space management"]
[float]
==== Delete a space
-Deleting a space permanently removes the space and all of its contents.
+Deleting a space permanently removes the space and all of its contents.
Find the space on the *Spaces* overview page and click the trash icon in the Actions column.
You can't delete the default space, but you can customize it to your liking.
@@ -63,14 +63,14 @@ You can't delete the default space, but you can customize it to your liking.
[[spaces-control-feature-visibility]]
=== Control feature access based on user needs
-You have control over which features are visible in each space.
-For example, you might hide Dev Tools
+You have control over which features are visible in each space.
+For example, you might hide Dev Tools
in your "Executive" space or show Stack Monitoring only in your "Admin" space.
You can define which features to show or hide when you add or edit a space.
-Controlling feature
-visibility is not a security feature. To secure access
-to specific features on a per-user basis, you must configure
+Controlling feature
+visibility is not a security feature. To secure access
+to specific features on a per-user basis, you must configure
<>.
[role="screenshot"]
@@ -80,10 +80,10 @@ image::spaces/images/edit-space-feature-visibility.png["Controlling features vis
[[spaces-control-user-access]]
=== Control feature access based on user privileges
-When using Kibana with security, you can configure applications and features
-based on your users’ privileges. This means different roles can have access
-to different features in the same space.
-Power users might have privileges to create and edit visualizations and dashboards,
+When using Kibana with security, you can configure applications and features
+based on your users’ privileges. This means different roles can have access
+to different features in the same space.
+Power users might have privileges to create and edit visualizations and dashboards,
while analysts or executives might have Dashboard and Canvas with read-only privileges.
See <> for details.
@@ -106,7 +106,7 @@ interface.
. Import your saved objects.
. (Optional) Delete objects in the export space that you no longer need.
-{kib} also has beta <> and
+{kib} also has beta <> and
<> APIs if you want to automate this process.
[float]
@@ -115,17 +115,22 @@ interface.
You can create a custom experience for users by configuring the {kib} landing page on a per-space basis.
The landing page can route users to a specific dashboard, application, or saved object as they enter each space.
-To configure the landing page, use the `defaultRoute` setting in < Advanced settings>>.
+
+To configure the landing page, use the default route setting in < Advanced settings>>.
+For example, you might set the default route to `/app/kibana#/dashboards`.
+
+[role="screenshot"]
+image::spaces/images/spaces-configure-landing-page.png["Configure space-level landing page"]
+
[float]
[[spaces-delete-started]]
=== Disable and version updates
-Spaces are automatically enabled in {kib}. If you don't want use this feature,
+Spaces are automatically enabled in {kib}. If you don't want use this feature,
you can disable it
-by setting `xpack.spaces.enabled` to `false` in your
+by setting `xpack.spaces.enabled` to `false` in your
`kibana.yml` configuration file.
-If you are upgrading your
-version of {kib}, the default space will contain all of your existing saved objects.
-
+If you are upgrading your
+version of {kib}, the default space will contain all of your existing saved objects.
diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc
index d7af0d5c420a1..b5d263aed8346 100644
--- a/docs/user/monitoring/monitoring-kibana.asciidoc
+++ b/docs/user/monitoring/monitoring-kibana.asciidoc
@@ -96,7 +96,8 @@ used when {kib} sends monitoring data to the production cluster.
.. Configure {kib} to encrypt communications between the {kib} server and the
production cluster. This set up involves generating a server certificate and
setting `server.ssl.*` and `elasticsearch.ssl.certificateAuthorities` settings
-in the `kibana.yml` file on the {kib} server. For example:
+in the `kibana.yml` file on the {kib} server. For example, using a PEM-formatted
+certificate and private key:
+
--
[source,yaml]
@@ -105,14 +106,19 @@ server.ssl.key: /path/to/your/server.key
server.ssl.certificate: /path/to/your/server.crt
--------------------------------------------------------------------------------
-If you are using your own certificate authority to sign certificates, specify
-the location of the PEM file in the `kibana.yml` file:
+If you are using your own certificate authority (CA) to sign certificates,
+specify the location of the PEM file in the `kibana.yml` file:
[source,yaml]
--------------------------------------------------------------------------------
elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem
--------------------------------------------------------------------------------
+NOTE: Alternatively, the PKCS #12 format can be used for the Kibana certificate
+and key, along with any included CA certificates, by setting
+`server.ssl.keystore.path`. If your CA certificate chain is in a separate trust
+store, you can also use `server.ssl.truststore.path`.
+
For more information, see <>.
--
diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc
index 2e2aaf688e8b6..05aabfc343be9 100644
--- a/docs/user/security/authentication/index.asciidoc
+++ b/docs/user/security/authentication/index.asciidoc
@@ -67,6 +67,9 @@ server.ssl.clientAuthentication: required
xpack.security.authc.providers: [pki]
--------------------------------------------------------------------------------
+NOTE: Trusted CAs can also be specified in a PKCS #12 keystore bundled with your Kibana server certificate/key using
+`server.ssl.keystore.path` or in a separate trust store using `server.ssl.truststore.path`.
+
PKI support in {kib} is designed to be the primary (or sole) authentication method for users of that {kib} instance. However, you can configure both PKI and Basic authentication for the same {kib} instance:
[source,yaml]
diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc
index 6917a48909c7b..b370c35905bce 100644
--- a/docs/user/security/securing-communications/index.asciidoc
+++ b/docs/user/security/securing-communications/index.asciidoc
@@ -4,121 +4,115 @@
Encrypting communications
++++
-{kib} supports Transport Layer Security (TLS/SSL) encryption for client
-requests.
-//TBD: It is unclear what "client requests" are in this context. Is it just
-// communication between the browser and the Kibana server or are we talking
-// about other types of clients connecting to the Kibana server?
-
-If you are using {security} or a proxy that provides an HTTPS endpoint for {es},
-you can configure {kib} to access {es} via HTTPS. Thus, communications between
-{kib} and {es} are also encrypted.
-
-. Configure {kib} to encrypt communications between the browser and the {kib}
-server:
+{kib} supports Transport Layer Security (TLS/SSL) encryption for all forms of data-in-transit. Browsers send traffic to {kib} and {kib}
+sends traffic to {es}. These communications are configured separately.
+
+[[configuring-tls-browser-kib]]
+==== Encrypting traffic between the browser and {kib}
+
+NOTE: You do not need to enable {security-features} for this type of encryption.
+
+. Obtain a server certificate and private key for {kib}.
+
--
-NOTE: You do not need to enable {security} for this type of encryption.
+{kib} supports certificates/keys in both PKCS #12 key stores and PEM format.
+
+When you obtain a certificate, you must do at least one of the following:
+
+.. Set the certificate's `subjectAltName` to the hostname, fully-qualified domain name (FQDN), or IP address of the {kib} server.
+
+.. Set the certificate's Common Name (CN) to the {kib} server's hostname or FQDN. Using the server's IP address as the CN does not work.
+
+You may choose to generate a certificate and private key using {ref}/certutil.html[the {es} certutil tool]. If you already used certutil to
+generate a certificate authority (CA), you would generate a certificate/key for Kibana like so (using the `--dns` param to set the
+`subjectAltName`):
+
+[source,sh]
+--------------------------------------------------------------------------------
+bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12 --name kibana --dns localhost
+--------------------------------------------------------------------------------
+
+This will generate a certificate and private key in a PKCS #12 keystore named `kibana.p12`.
--
-.. Generate a server certificate for {kib}.
+. Enable TLS/SSL in `kibana.yml`:
+
--
-//TBD: Can we provide more information about how they generate the certificate?
-//Would they be able to use something like the elasticsearch-certutil command?
-You must either set the certificate's
-`subjectAltName` to the hostname, fully-qualified domain name (FQDN), or IP
-address of the {kib} server, or set the CN to the {kib} server's hostname
-or FQDN. Using the server's IP address as the CN does not work.
+[source,yaml]
+--------------------------------------------------------------------------------
+server.ssl.enabled: true
+--------------------------------------------------------------------------------
--
-.. Set the `server.ssl.enabled`, `server.ssl.key`, and `server.ssl.certificate`
-properties in `kibana.yml`:
+. Specify your server certificate and private key in `kibana.yml`:
+
--
+If your certificate and private key are in a PKCS #12 keystore, specify it like so:
+
[source,yaml]
--------------------------------------------------------------------------------
-server.ssl.enabled: true
-server.ssl.key: /path/to/your/server.key
-server.ssl.certificate: /path/to/your/server.crt
+server.ssl.keystore.path: "/path/to/your/keystore.p12"
+server.ssl.keystore.password: "optional decryption password"
+--------------------------------------------------------------------------------
+
+Otherwise, if your certificate/key are in PEM format, specify them like so:
+
+[source,yaml]
+--------------------------------------------------------------------------------
+server.ssl.certificate: "/path/to/your/server.crt"
+server.ssl.key: "/path/to/your/server.key"
+server.ssl.keyPassphrase: "optional decryption password"
--------------------------------------------------------------------------------
After making these changes, you must always access {kib} via HTTPS. For example,
https://localhost:5601.
-// TBD: The reference information for server.ssl.enabled says it "enables SSL for
-// outgoing requests from the Kibana server to the browser". Do we need to
-// reiterate here that only one side of the communications is encrypted?
-
For more information, see <>.
--
-. Configure {kib} to connect to {es} via HTTPS:
-+
---
+[[configuring-tls-kib-es]]
+==== Encrypting traffic between {kib} and {es}
+
NOTE: To perform this step, you must
{ref}/configuring-security.html[enable the {es} {security-features}] or you
must have a proxy that provides an HTTPS endpoint for {es}.
---
-
-.. Specify the HTTPS protocol in the `elasticsearch.hosts` setting in the {kib}
-configuration file, `kibana.yml`:
+. Specify the HTTPS URL in the `elasticsearch.hosts` setting in the {kib} configuration file, `kibana.yml`:
+
--
[source,yaml]
--------------------------------------------------------------------------------
elasticsearch.hosts: ["https://.com:9200"]
--------------------------------------------------------------------------------
---
-
-.. If you are using your own CA to sign certificates for {es}, set the
-`elasticsearch.ssl.certificateAuthorities` setting in `kibana.yml` to specify
-the location of the PEM file.
-+
---
-[source,yaml]
---------------------------------------------------------------------------------
-elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem
---------------------------------------------------------------------------------
-Setting the `certificateAuthorities` property lets you use the default
-`verificationMode` option of `full`.
-//TBD: Is this still true? It isn't mentioned in https://www.elastic.co/guide/en/kibana/master/settings.html
+Using the HTTPS protocol results in a default `elasticsearch.ssl.verificationMode` option of `full`, which utilizes hostname verification.
For more information, see <>.
--
-. (Optional) If the Elastic {monitor-features} are enabled, configure {kib} to
-connect to the {es} monitoring cluster via HTTPS:
+. Specify the {es} cluster's CA certificate chain in `kibana.yml`:
+
--
-NOTE: To perform this step, you must
-{ref}/configuring-security.html[enable the {es} {security-features}] or you
-must have a proxy that provides an HTTPS endpoint for {es}.
---
+If you are using your own CA to sign certificates for {es}, then you need to specify the CA certificate chain in {kib} to properly establish
+trust in TLS connections. If your CA certificate chain is contained in a PKCS #12 trust store, specify it like so:
-.. Specify the HTTPS URL in the `xpack.monitoring.elasticsearch.hosts` setting in
-the {kib} configuration file, `kibana.yml`
-+
---
[source,yaml]
--------------------------------------------------------------------------------
-xpack.monitoring.elasticsearch.hosts: ["https://:9200"]
+elasticsearch.ssl.truststore.path: "/path/to/your/truststore.p12"
+elasticsearch.ssl.truststore.password: "optional decryption password"
--------------------------------------------------------------------------------
---
-.. Specify the `xpack.monitoring.elasticsearch.ssl.*` settings in the
-`kibana.yml` file.
-+
---
-For example, if you are using your own certificate authority to sign
-certificates, specify the location of the PEM file in the `kibana.yml` file:
+Otherwise, if your CA certificate chain is in PEM format, specify each certificate like so:
[source,yaml]
--------------------------------------------------------------------------------
-xpack.monitoring.elasticsearch.ssl.certificateAuthorities: /path/to/your/cacert.pem
+elasticsearch.ssl.certificateAuthorities: ["/path/to/your/cacert1.pem", "/path/to/your/cacert2.pem"]
--------------------------------------------------------------------------------
+
--
+
+. (Optional) If the Elastic {monitor-features} are enabled, configure {kib} to connect to the {es} monitoring cluster via HTTPS. The steps
+are the same as above, but each setting is prefixed by `xpack.monitoring.`. For example, `xpack.monitoring.elasticsearch.hosts`,
+`xpack.monitoring.elasticsearch.ssl.truststore.path`, etc.
diff --git a/examples/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts
index 23c82225563c8..653aa217717fa 100644
--- a/examples/demo_search/server/plugin.ts
+++ b/examples/demo_search/server/plugin.ts
@@ -18,7 +18,7 @@
*/
import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server';
-import { DataPluginSetup } from 'src/plugins/data/server/plugin';
+import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server';
import { demoSearchStrategyProvider } from './demo_search_strategy';
import { DEMO_SEARCH_STRATEGY, IDemoRequest, IDemoResponse } from '../common';
diff --git a/package.json b/package.json
index db5764e6e91ba..a91e9b80eb49d 100644
--- a/package.json
+++ b/package.json
@@ -114,7 +114,7 @@
"@babel/core": "^7.5.5",
"@babel/register": "^7.7.0",
"@elastic/apm-rum": "^4.6.0",
- "@elastic/charts": "^16.0.2",
+ "@elastic/charts": "^16.1.0",
"@elastic/datemath": "5.0.2",
"@elastic/ems-client": "1.0.5",
"@elastic/eui": "17.3.1",
@@ -135,6 +135,7 @@
"@kbn/ui-framework": "1.0.0",
"@types/json-stable-stringify": "^1.0.32",
"@types/lodash.clonedeep": "^4.5.4",
+ "@types/node-forge": "^0.9.0",
"@types/react-grid-layout": "^0.16.7",
"@types/recompose": "^0.30.5",
"JSONStream": "1.3.5",
@@ -217,6 +218,7 @@
"mustache": "2.3.2",
"ngreact": "0.5.1",
"node-fetch": "1.7.3",
+ "node-forge": "^0.9.1",
"opn": "^5.5.0",
"oppsy": "^2.0.0",
"pegjs": "0.10.0",
@@ -341,6 +343,7 @@
"@types/mustache": "^0.8.31",
"@types/node": "^10.12.27",
"@types/opn": "^5.1.0",
+ "@types/pegjs": "^0.10.1",
"@types/pngjs": "^3.3.2",
"@types/podium": "^1.0.0",
"@types/prop-types": "^15.5.3",
diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md
index fd62f1b3c03b2..e6f3e60128983 100644
--- a/packages/kbn-config-schema/README.md
+++ b/packages/kbn-config-schema/README.md
@@ -156,6 +156,9 @@ __Usage:__
const valueSchema = schema.boolean({ defaultValue: false });
```
+__Notes:__
+* The `schema.boolean()` also supports a string as input if it equals `'true'` or `'false'` (case-insensitive).
+
#### `schema.literal()`
Validates input data as a [string](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types), [numeric](https://www.typescriptlang.org/docs/handbook/advanced-types.html#numeric-literal-types) or boolean literal.
@@ -397,7 +400,7 @@ const valueSchema = schema.byteSize({ min: '3kb' });
```
__Notes:__
-* The string value for `schema.byteSize()` and its options supports the following prefixes: `b`, `kb`, `mb`, `gb` and `tb`.
+* The string value for `schema.byteSize()` and its options supports the following optional suffixes: `b`, `kb`, `mb`, `gb` and `tb`. The default suffix is `b`.
* The number value is treated as a number of bytes and hence should be a positive integer, e.g. `100` is equal to `'100b'`.
* Currently you cannot specify zero bytes with a string format and should use number `0` instead.
@@ -417,7 +420,7 @@ const valueSchema = schema.duration({ defaultValue: '70ms' });
```
__Notes:__
-* The string value for `schema.duration()` supports the following prefixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`.
+* The string value for `schema.duration()` supports the following optional suffixes: `ms`, `s`, `m`, `h`, `d`, `w`, `M` and `Y`. The default suffix is `ms`.
* The number value is treated as a number of milliseconds and hence should be a positive integer, e.g. `100` is equal to `'100ms'`.
#### `schema.conditional()`
diff --git a/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap b/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap
index 1db6930062a9a..97e9082401b3d 100644
--- a/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap
+++ b/packages/kbn-config-schema/src/byte_size_value/__snapshots__/index.test.ts.snap
@@ -1,9 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`#constructor throws if number of bytes is negative 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-1024]"`;
+exports[`#constructor throws if number of bytes is negative 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-1024]."`;
-exports[`#constructor throws if number of bytes is not safe 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]"`;
+exports[`#constructor throws if number of bytes is not safe 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]."`;
-exports[`#constructor throws if number of bytes is not safe 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]"`;
+exports[`#constructor throws if number of bytes is not safe 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]."`;
-exports[`#constructor throws if number of bytes is not safe 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]"`;
+exports[`#constructor throws if number of bytes is not safe 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]."`;
+
+exports[`parsing units throws an error when unsupported unit specified 1`] = `"Failed to parse [1tb] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`;
diff --git a/packages/kbn-config-schema/src/byte_size_value/index.test.ts b/packages/kbn-config-schema/src/byte_size_value/index.test.ts
index 46ed96c83dd1f..198d95aa0ab4c 100644
--- a/packages/kbn-config-schema/src/byte_size_value/index.test.ts
+++ b/packages/kbn-config-schema/src/byte_size_value/index.test.ts
@@ -20,6 +20,10 @@
import { ByteSizeValue } from '.';
describe('parsing units', () => {
+ test('number string (bytes)', () => {
+ expect(ByteSizeValue.parse('123').getValueInBytes()).toBe(123);
+ });
+
test('bytes', () => {
expect(ByteSizeValue.parse('123b').getValueInBytes()).toBe(123);
});
@@ -37,12 +41,8 @@ describe('parsing units', () => {
expect(ByteSizeValue.parse('1gb').getValueInBytes()).toBe(1073741824);
});
- test('throws an error when no unit specified', () => {
- expect(() => ByteSizeValue.parse('123')).toThrowError('could not parse byte size value');
- });
-
test('throws an error when unsupported unit specified', () => {
- expect(() => ByteSizeValue.parse('1tb')).toThrowError('could not parse byte size value');
+ expect(() => ByteSizeValue.parse('1tb')).toThrowErrorMatchingSnapshot();
});
});
diff --git a/packages/kbn-config-schema/src/byte_size_value/index.ts b/packages/kbn-config-schema/src/byte_size_value/index.ts
index fb0105503a149..48862821bb78d 100644
--- a/packages/kbn-config-schema/src/byte_size_value/index.ts
+++ b/packages/kbn-config-schema/src/byte_size_value/index.ts
@@ -35,9 +35,14 @@ export class ByteSizeValue {
public static parse(text: string): ByteSizeValue {
const match = /([1-9][0-9]*)(b|kb|mb|gb)/.exec(text);
if (!match) {
- throw new Error(
- `could not parse byte size value [${text}]. Value must be a safe positive integer.`
- );
+ const number = Number(text);
+ if (typeof number !== 'number' || isNaN(number)) {
+ throw new Error(
+ `Failed to parse [${text}] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] ` +
+ `(e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer.`
+ );
+ }
+ return new ByteSizeValue(number);
}
const value = parseInt(match[1], 0);
@@ -49,8 +54,7 @@ export class ByteSizeValue {
constructor(private readonly valueInBytes: number) {
if (!Number.isSafeInteger(valueInBytes) || valueInBytes < 0) {
throw new Error(
- `Value in bytes is expected to be a safe positive integer, ` +
- `but provided [${valueInBytes}]`
+ `Value in bytes is expected to be a safe positive integer, but provided [${valueInBytes}].`
);
}
}
diff --git a/packages/kbn-config-schema/src/duration/index.ts b/packages/kbn-config-schema/src/duration/index.ts
index ff8f96614a193..b96b5a3687bbb 100644
--- a/packages/kbn-config-schema/src/duration/index.ts
+++ b/packages/kbn-config-schema/src/duration/index.ts
@@ -25,10 +25,14 @@ const timeFormatRegex = /^(0|[1-9][0-9]*)(ms|s|m|h|d|w|M|Y)$/;
function stringToDuration(text: string) {
const result = timeFormatRegex.exec(text);
if (!result) {
- throw new Error(
- `Failed to parse [${text}] as time value. ` +
- `Format must be [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y')`
- );
+ const number = Number(text);
+ if (typeof number !== 'number' || isNaN(number)) {
+ throw new Error(
+ `Failed to parse [${text}] as time value. Value must be a duration in milliseconds, or follow the format ` +
+ `[ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer.`
+ );
+ }
+ return numberToDuration(number);
}
const count = parseInt(result[1], 0);
@@ -40,8 +44,7 @@ function stringToDuration(text: string) {
function numberToDuration(numberMs: number) {
if (!Number.isSafeInteger(numberMs) || numberMs < 0) {
throw new Error(
- `Failed to parse [${numberMs}] as time value. ` +
- `Value should be a safe positive integer number.`
+ `Value in milliseconds is expected to be a safe positive integer, but provided [${numberMs}].`
);
}
diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts
index 4d5091eaa09b1..044c3050f9fa8 100644
--- a/packages/kbn-config-schema/src/internals/index.ts
+++ b/packages/kbn-config-schema/src/internals/index.ts
@@ -82,7 +82,23 @@ export const internals = Joi.extend([
base: Joi.boolean(),
coerce(value: any, state: State, options: ValidationOptions) {
// If value isn't defined, let Joi handle default value if it's defined.
- if (value !== undefined && typeof value !== 'boolean') {
+ if (value === undefined) {
+ return value;
+ }
+
+ // Allow strings 'true' and 'false' to be coerced to booleans (case-insensitive).
+
+ // From Joi docs on `Joi.boolean`:
+ // > Generates a schema object that matches a boolean data type. Can also
+ // > be called via bool(). If the validation convert option is on
+ // > (enabled by default), a string (either "true" or "false") will be
+ // converted to a boolean if specified.
+ if (typeof value === 'string') {
+ const normalized = value.toLowerCase();
+ value = normalized === 'true' ? true : normalized === 'false' ? false : value;
+ }
+
+ if (typeof value !== 'boolean') {
return this.createError('boolean.base', { value }, state, options);
}
diff --git a/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap
index c3f33dc29bf50..0e5f6de2deea8 100644
--- a/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap
+++ b/packages/kbn-config-schema/src/types/__snapshots__/boolean_type.test.ts.snap
@@ -9,3 +9,7 @@ exports[`returns error when not boolean 1`] = `"expected value of type [boolean]
exports[`returns error when not boolean 2`] = `"expected value of type [boolean] but got [Array]"`;
exports[`returns error when not boolean 3`] = `"expected value of type [boolean] but got [string]"`;
+
+exports[`returns error when not boolean 4`] = `"expected value of type [boolean] but got [number]"`;
+
+exports[`returns error when not boolean 5`] = `"expected value of type [boolean] but got [string]"`;
diff --git a/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap
index f6f45a96ca161..ea2102b1776fb 100644
--- a/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap
+++ b/packages/kbn-config-schema/src/types/__snapshots__/byte_size_type.test.ts.snap
@@ -18,6 +18,12 @@ ByteSizeValue {
}
`;
+exports[`#defaultValue can be a string-formatted number 1`] = `
+ByteSizeValue {
+ "valueInBytes": 1024,
+}
+`;
+
exports[`#max returns error when larger 1`] = `"Value is [1mb] ([1048576b]) but it must be equal to or less than [1kb]"`;
exports[`#max returns value when smaller 1`] = `
@@ -38,20 +44,18 @@ exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value o
exports[`is required by default 1`] = `"expected value of type [ByteSize] but got [undefined]"`;
-exports[`returns error when not string or positive safe integer 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-123]"`;
+exports[`returns error when not valid string or positive safe integer 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-123]."`;
-exports[`returns error when not string or positive safe integer 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]"`;
+exports[`returns error when not valid string or positive safe integer 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]."`;
-exports[`returns error when not string or positive safe integer 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]"`;
+exports[`returns error when not valid string or positive safe integer 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]."`;
-exports[`returns error when not string or positive safe integer 4`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]"`;
+exports[`returns error when not valid string or positive safe integer 4`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]."`;
-exports[`returns error when not string or positive safe integer 5`] = `"expected value of type [ByteSize] but got [Array]"`;
+exports[`returns error when not valid string or positive safe integer 5`] = `"expected value of type [ByteSize] but got [Array]"`;
-exports[`returns error when not string or positive safe integer 6`] = `"expected value of type [ByteSize] but got [RegExp]"`;
+exports[`returns error when not valid string or positive safe integer 6`] = `"expected value of type [ByteSize] but got [RegExp]"`;
-exports[`returns value by default 1`] = `
-ByteSizeValue {
- "valueInBytes": 123,
-}
-`;
+exports[`returns error when not valid string or positive safe integer 7`] = `"Failed to parse [123foo] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`;
+
+exports[`returns error when not valid string or positive safe integer 8`] = `"Failed to parse [123 456] as byte value. Value must be either number of bytes, or follow the format [b|kb|mb|gb] (e.g., '1024kb', '200mb', '1gb'), where the number is a safe positive integer."`;
diff --git a/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap
index a21c28e7cc614..c4e4ff652a2d7 100644
--- a/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap
+++ b/packages/kbn-config-schema/src/types/__snapshots__/duration_type.test.ts.snap
@@ -6,20 +6,24 @@ exports[`#defaultValue can be a number 1`] = `"PT0.6S"`;
exports[`#defaultValue can be a string 1`] = `"PT1H"`;
+exports[`#defaultValue can be a string-formatted number 1`] = `"PT0.6S"`;
+
exports[`includes namespace in failure 1`] = `"[foo-namespace]: expected value of type [moment.Duration] but got [undefined]"`;
exports[`is required by default 1`] = `"expected value of type [moment.Duration] but got [undefined]"`;
-exports[`returns error when not string or non-safe positive integer 1`] = `"Failed to parse [-123] as time value. Value should be a safe positive integer number."`;
+exports[`returns error when not valid string or non-safe positive integer 1`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [-123]."`;
+
+exports[`returns error when not valid string or non-safe positive integer 2`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [NaN]."`;
-exports[`returns error when not string or non-safe positive integer 2`] = `"Failed to parse [NaN] as time value. Value should be a safe positive integer number."`;
+exports[`returns error when not valid string or non-safe positive integer 3`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [Infinity]."`;
-exports[`returns error when not string or non-safe positive integer 3`] = `"Failed to parse [Infinity] as time value. Value should be a safe positive integer number."`;
+exports[`returns error when not valid string or non-safe positive integer 4`] = `"Value in milliseconds is expected to be a safe positive integer, but provided [9007199254740992]."`;
-exports[`returns error when not string or non-safe positive integer 4`] = `"Failed to parse [9007199254740992] as time value. Value should be a safe positive integer number."`;
+exports[`returns error when not valid string or non-safe positive integer 5`] = `"expected value of type [moment.Duration] but got [Array]"`;
-exports[`returns error when not string or non-safe positive integer 5`] = `"expected value of type [moment.Duration] but got [Array]"`;
+exports[`returns error when not valid string or non-safe positive integer 6`] = `"expected value of type [moment.Duration] but got [RegExp]"`;
-exports[`returns error when not string or non-safe positive integer 6`] = `"expected value of type [moment.Duration] but got [RegExp]"`;
+exports[`returns error when not valid string or non-safe positive integer 7`] = `"Failed to parse [123foo] as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."`;
-exports[`returns value by default 1`] = `"PT2M3S"`;
+exports[`returns error when not valid string or non-safe positive integer 8`] = `"Failed to parse [123 456] as time value. Value must be a duration in milliseconds, or follow the format [ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y'), where the duration is a safe positive integer."`;
diff --git a/packages/kbn-config-schema/src/types/boolean_type.test.ts b/packages/kbn-config-schema/src/types/boolean_type.test.ts
index d6e274f05e3ff..e94999b505437 100644
--- a/packages/kbn-config-schema/src/types/boolean_type.test.ts
+++ b/packages/kbn-config-schema/src/types/boolean_type.test.ts
@@ -23,6 +23,17 @@ test('returns value by default', () => {
expect(schema.boolean().validate(true)).toBe(true);
});
+test('handles boolean strings', () => {
+ expect(schema.boolean().validate('true')).toBe(true);
+ expect(schema.boolean().validate('TRUE')).toBe(true);
+ expect(schema.boolean().validate('True')).toBe(true);
+ expect(schema.boolean().validate('TrUe')).toBe(true);
+ expect(schema.boolean().validate('false')).toBe(false);
+ expect(schema.boolean().validate('FALSE')).toBe(false);
+ expect(schema.boolean().validate('False')).toBe(false);
+ expect(schema.boolean().validate('FaLse')).toBe(false);
+});
+
test('is required by default', () => {
expect(() => schema.boolean().validate(undefined)).toThrowErrorMatchingSnapshot();
});
@@ -49,4 +60,8 @@ test('returns error when not boolean', () => {
expect(() => schema.boolean().validate([1, 2, 3])).toThrowErrorMatchingSnapshot();
expect(() => schema.boolean().validate('abc')).toThrowErrorMatchingSnapshot();
+
+ expect(() => schema.boolean().validate(0)).toThrowErrorMatchingSnapshot();
+
+ expect(() => schema.boolean().validate('no')).toThrowErrorMatchingSnapshot();
});
diff --git a/packages/kbn-config-schema/src/types/byte_size_type.test.ts b/packages/kbn-config-schema/src/types/byte_size_type.test.ts
index 67eae1e7c382a..7c65ec2945b49 100644
--- a/packages/kbn-config-schema/src/types/byte_size_type.test.ts
+++ b/packages/kbn-config-schema/src/types/byte_size_type.test.ts
@@ -23,7 +23,15 @@ import { ByteSizeValue } from '../byte_size_value';
const { byteSize } = schema;
test('returns value by default', () => {
- expect(byteSize().validate('123b')).toMatchSnapshot();
+ expect(byteSize().validate('123b')).toEqual(new ByteSizeValue(123));
+});
+
+test('handles numeric strings', () => {
+ expect(byteSize().validate('123')).toEqual(new ByteSizeValue(123));
+});
+
+test('handles numbers', () => {
+ expect(byteSize().validate(123)).toEqual(new ByteSizeValue(123));
});
test('is required by default', () => {
@@ -51,6 +59,14 @@ describe('#defaultValue', () => {
).toMatchSnapshot();
});
+ test('can be a string-formatted number', () => {
+ expect(
+ byteSize({
+ defaultValue: '1024',
+ }).validate(undefined)
+ ).toMatchSnapshot();
+ });
+
test('can be a number', () => {
expect(
byteSize({
@@ -88,7 +104,7 @@ describe('#max', () => {
});
});
-test('returns error when not string or positive safe integer', () => {
+test('returns error when not valid string or positive safe integer', () => {
expect(() => byteSize().validate(-123)).toThrowErrorMatchingSnapshot();
expect(() => byteSize().validate(NaN)).toThrowErrorMatchingSnapshot();
@@ -100,4 +116,8 @@ test('returns error when not string or positive safe integer', () => {
expect(() => byteSize().validate([1, 2, 3])).toThrowErrorMatchingSnapshot();
expect(() => byteSize().validate(/abc/)).toThrowErrorMatchingSnapshot();
+
+ expect(() => byteSize().validate('123foo')).toThrowErrorMatchingSnapshot();
+
+ expect(() => byteSize().validate('123 456')).toThrowErrorMatchingSnapshot();
});
diff --git a/packages/kbn-config-schema/src/types/duration_type.test.ts b/packages/kbn-config-schema/src/types/duration_type.test.ts
index 39655d43d7b75..09e92ce727f2a 100644
--- a/packages/kbn-config-schema/src/types/duration_type.test.ts
+++ b/packages/kbn-config-schema/src/types/duration_type.test.ts
@@ -23,7 +23,15 @@ import { schema } from '..';
const { duration, object, contextRef, siblingRef } = schema;
test('returns value by default', () => {
- expect(duration().validate('123s')).toMatchSnapshot();
+ expect(duration().validate('123s')).toEqual(momentDuration(123000));
+});
+
+test('handles numeric string', () => {
+ expect(duration().validate('123000')).toEqual(momentDuration(123000));
+});
+
+test('handles number', () => {
+ expect(duration().validate(123000)).toEqual(momentDuration(123000));
});
test('is required by default', () => {
@@ -51,6 +59,14 @@ describe('#defaultValue', () => {
).toMatchSnapshot();
});
+ test('can be a string-formatted number', () => {
+ expect(
+ duration({
+ defaultValue: '600',
+ }).validate(undefined)
+ ).toMatchSnapshot();
+ });
+
test('can be a number', () => {
expect(
duration({
@@ -124,7 +140,7 @@ Object {
});
});
-test('returns error when not string or non-safe positive integer', () => {
+test('returns error when not valid string or non-safe positive integer', () => {
expect(() => duration().validate(-123)).toThrowErrorMatchingSnapshot();
expect(() => duration().validate(NaN)).toThrowErrorMatchingSnapshot();
@@ -136,4 +152,8 @@ test('returns error when not string or non-safe positive integer', () => {
expect(() => duration().validate([1, 2, 3])).toThrowErrorMatchingSnapshot();
expect(() => duration().validate(/abc/)).toThrowErrorMatchingSnapshot();
+
+ expect(() => duration().validate('123foo')).toThrowErrorMatchingSnapshot();
+
+ expect(() => duration().validate('123 456')).toThrowErrorMatchingSnapshot();
});
diff --git a/packages/kbn-dev-utils/certs/README.md b/packages/kbn-dev-utils/certs/README.md
new file mode 100644
index 0000000000000..fdf7892789404
--- /dev/null
+++ b/packages/kbn-dev-utils/certs/README.md
@@ -0,0 +1,62 @@
+# Development certificates
+
+Kibana includes several development certificates to enable easy setup of TLS-encrypted communications with Elasticsearch.
+
+_Note: these certificates should **never** be used in production._
+
+## Certificate information
+
+Certificates and keys are provided in multiple formats. These can be used by other packages to set up a new Elastic Stack with Kibana and Elasticsearch. The Certificate Authority (CA) private key is intentionally omitted from this package.
+
+### PEM
+
+* `ca.crt` -- A [PEM-formatted](https://tools.ietf.org/html/rfc1421) [X.509](https://tools.ietf.org/html/rfc5280) certificate that is used as a CA.
+* `elasticsearch.crt` -- A PEM-formatted X.509 certificate and public key for Elasticsearch.
+* `elasticsearch.key` -- A PEM-formatted [PKCS #1](https://tools.ietf.org/html/rfc8017) private key for Elasticsearch.
+* `kibana.crt` -- A PEM-formatted X.509 certificate and public key for Kibana.
+* `kibana.key` -- A PEM-formatted PKCS #1 private key for Kibana.
+
+### PKCS #12
+
+* `elasticsearch.p12` -- A [PKCS #12](https://tools.ietf.org/html/rfc7292) encrypted key store / trust store that contains `ca.crt`, `elasticsearch.crt`, and a [PKCS #8](https://tools.ietf.org/html/rfc5208) encrypted version of `elasticsearch.key`.
+* `kibana.p12` -- A PKCS #12 encrypted key store / trust store that contains `ca.crt`, `kibana.crt`, and a PKCS #8 encrypted version of `kibana.key`.
+
+The password used for both of these is "storepass". Other copies are also provided for testing purposes:
+
+* `elasticsearch_emptypassword.p12` -- The same PKCS #12 key store, encrypted with an empty password.
+* `elasticsearch_nopassword.p12` -- The same PKCS #12 key store, not encrypted with a password.
+
+## Certificate generation
+
+[Elasticsearch cert-util](https://www.elastic.co/guide/en/elasticsearch/reference/current/certutil.html) and [OpenSSL](https://www.openssl.org/) were used to generate these certificates. The following commands were used from the root directory of Elasticsearch:
+
+```
+# Generate the PKCS #12 keystore for a CA, valid for 50 years
+bin/elasticsearch-certutil ca -days 18250 --pass castorepass
+
+# Generate the PKCS #12 keystore for Elasticsearch and sign it with the CA
+bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name elasticsearch --dns localhost --pass storepass
+
+# Generate the PKCS #12 keystore for Kibana and sign it with the CA
+bin/elasticsearch-certutil cert -days 18250 --ca elastic-stack-ca.p12 --ca-pass castorepass --name kibana --dns localhost --pass storepass
+
+# Copy the PKCS #12 keystore for Elasticsearch with an empty password
+openssl pkcs12 -in elasticsearch.p12 -nodes -passin pass:"storepass" -passout pass:"" | openssl pkcs12 -export -out elasticsearch_emptypassword.p12 -passout pass:""
+
+# Manually create "elasticsearch_nopassword.p12" -- this can be done on macOS by importing the P12 key store into the Keychain and exporting it again
+
+# Extract the PEM-formatted X.509 certificate for the CA
+openssl pkcs12 -in elasticsearch.p12 -out ca.crt -cacerts -passin pass:"storepass" -passout pass:
+
+# Extract the PEM-formatted PKCS #1 private key for Elasticsearch
+openssl pkcs12 -in elasticsearch.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:keypass -out elasticsearch.key
+
+# Extract the PEM-formatted X.509 certificate for Elasticsearch
+openssl pkcs12 -in elasticsearch.p12 -out elasticsearch.crt -clcerts -passin pass:"storepass" -passout pass:
+
+# Extract the PEM-formatted PKCS #1 private key for Kibana
+openssl pkcs12 -in kibana.p12 -nocerts -passin pass:"storepass" -passout pass:"keypass" | openssl rsa -passin pass:keypass -out kibana.key
+
+# Extract the PEM-formatted X.509 certificate for Kibana
+openssl pkcs12 -in kibana.p12 -out kibana.crt -clcerts -passin pass:"storepass" -passout pass:
+```
diff --git a/packages/kbn-dev-utils/certs/ca.crt b/packages/kbn-dev-utils/certs/ca.crt
old mode 100755
new mode 100644
index 3e964823c5086..217935b8d83f6
--- a/packages/kbn-dev-utils/certs/ca.crt
+++ b/packages/kbn-dev-utils/certs/ca.crt
@@ -1,20 +1,29 @@
+Bag Attributes
+ friendlyName: elasticsearch
+ localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37
+Key Attributes:
+Bag Attributes
+ friendlyName: ca
+ 2.16.840.1.113894.746875.1.1:
+subject=/CN=Elastic Certificate Tool Autogenerated CA
+issuer=/CN=Elastic Certificate Tool Autogenerated CA
-----BEGIN CERTIFICATE-----
-MIIDSjCCAjKgAwIBAgIVAOgxLlE1RMGl2fYgTKDznvDL2vboMA0GCSqGSIb3DQEB
-CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu
-ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzQ0OFoXDTIyMDcxMDE3MzQ0OFowNDEyMDAG
-A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCNImKp/A9l++Ac7U5lvHOA
-+fYRb8p7AgdfKBMB0v3bo+bpHjkbkf3vYHjo1xJSg5ls6EPK+Do4owkAgKJdrznI
-5/efJOjgA+ylH4rgAfrRIQmiFEWZnAv86vJ+Iq83mfkPELb4dvXCi7AFQkzoM/rY
-Lbi97xha5bA2SEmpYp7VhBTM9zWy+q9Tm5odPO8u2n75GpIM2RwipaXlL0ink+06
-/oweQJoivaDgpDOmUXCFPmpV3VCdhUGxDQPyG0upQkF+NbQoei4RmluPEmVz4S7I
-TFLWjX7LeZVP63bJkcCgiq6Hm97kDtr9EYlPKhHm7UMWzhNzHbfvySMDzqAJC0KX
-AgMBAAGjUzBRMB0GA1UdDgQWBBRKqaaQ/+jT+ipPLJe7qekp1N/zizAfBgNVHSME
-GDAWgBRKqaaQ/+jT+ipPLJe7qekp1N/zizAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
-SIb3DQEBCwUAA4IBAQA7Gcq8h8yDXvepfKUAcTTMCBZkI+g3qE1gfRwjW7587CIj
-xnrzEqANU+Q1lv7IeQ158HiduDUMZfnvpuNwkf0HkqnRWb57RwfVdCAlAeZmzipq
-5ZJWlIW4dbmk57nGLg4fCszedi0uSGytZ2/BUdpWyC0fAM97h7Agtr4xGGKMEL67
-uB55ijt61V62HZ5wWXWNO9m+wfmdnt+YQViQJHtpYz1oOmWhY3dpitZLfWs1sLLD
-w3CZOhmWX7+P7+HlCkSBF4swzHOCI3THyX61NbLxju8VkTAjwbZPq4EOnVKnO6kr
-RdwQVnzKnqG5fxfSGknNahy0pOhJHZlGLwECRlgF
+MIIDSzCCAjOgAwIBAgIUW0brhEtYK3tUBYlXnUa+AMmAX6kwDQYJKoZIhvcNAQEL
+BQAwNDEyMDAGA1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5l
+cmF0ZWQgQ0EwIBcNMTkxMjI3MTcwMjMyWhgPMjA2OTEyMTQxNzAyMzJaMDQxMjAw
+BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENB
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAplO5m5Xy8xERyA0/G5SM
+Nu2QXkfS+m7ZTFjSmtwqX7BI1I6ISI4Yw8QxzcIgSbEGlSqb7baeT+A/1JQj0gZN
+KOnKbazl+ujVRJpsfpt5iUsnQyVPheGekcHkB+9WkZPgZ1oGRENr/4Eb1VImQf+Y
+yo/FUj8X939tYW0fficAqYKv8/4NWpBUbeop8wsBtkz738QKlmPkMwC4FbuF2/bN
+vNuzQuRbGMVmPeyivZJRfDAMKExoXjCCLmbShdg4dUHsUjVeWQZ6s4vbims+8qF9
+b4bseayScQNNU3hc5mkfhEhSM0KB0lDpSvoCxuXvXzb6bOk7xIdYo+O4vHUhvSkQ
+mwIDAQABo1MwUTAdBgNVHQ4EFgQUGu0mDnvDRnBdNBG8DxwPdWArB0kwHwYDVR0j
+BBgwFoAUGu0mDnvDRnBdNBG8DxwPdWArB0kwDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQsFAAOCAQEASv/FYOwWGnQreH8ulcVupGeZj25dIjZiuKfJmslH8QN/
+pVCIzAxNZjGjCpKxbJoCu5U9USaBylbhigeBJEq4wmYTs/WPu4uYMgDj0MILuHin
+RQqgEVG0uADGEgH2nnk8DeY8gQvGpJRQGlXNK8pb+pCsy6F8k/svGOeBND9osHfU
+CVEo5nXjfq6JCFt6hPx7kl4h3/j3C4wNy/Dv/QINdpPsl6CnF17Q9R9d60WFv42/
+pkl7W1hszCG9foNJOJabuWfVoPkvKQjoCvPitZt/hCaFZAW49PmAVhK+DAohQ91l
+TZhDmYqHoXNiRDQiUT68OS7RlfKgNpr/vMTZXDxpmw==
-----END CERTIFICATE-----
diff --git a/packages/kbn-dev-utils/certs/elasticsearch.crt b/packages/kbn-dev-utils/certs/elasticsearch.crt
old mode 100755
new mode 100644
index b30e11e9bbce1..87ba02019903f
--- a/packages/kbn-dev-utils/certs/elasticsearch.crt
+++ b/packages/kbn-dev-utils/certs/elasticsearch.crt
@@ -1,20 +1,29 @@
+Bag Attributes
+ friendlyName: elasticsearch
+ localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37
+Key Attributes:
+Bag Attributes
+ friendlyName: elasticsearch
+ localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 31 39 38 30 33 37
+subject=/CN=elasticsearch
+issuer=/CN=Elastic Certificate Tool Autogenerated CA
-----BEGIN CERTIFICATE-----
-MIIDRDCCAiygAwIBAgIVAI8V1fwvXKykKtp5k0cLpTOtY+DVMA0GCSqGSIb3DQEB
+MIIDQDCCAiigAwIBAgIVAI93OQE6tZffPyzenSg3ljE3JJBzMA0GCSqGSIb3DQEB
CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu
-ZXJhdGVkIENBMB4XDTE5MDcxMTE3MzUxOFoXDTIyMDcxMDE3MzUxOFowGDEWMBQG
-A1UEAxMNZWxhc3RpY3NlYXJjaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBALW+8gV6m6wYmTZmrXzNWKElE+ePkkikCviNfuWonWqxgAoWpAwAx2FvdhP3
-UDFbe38ydJX4oDgXeC25vdIR6z2uqzx+GXSNSybO7luuOUYQOP4Xf5Cj3zzXXMyu
-nY1nZTVsChI9jAMz4cZZdUd04f4r4TBNxrFCcVR0uec5RGRXuP8rSQd9AbYFUVYf
-jJeLb24asghb2Ku+c2JGvMqPEXFWFGOXFhUoIbRjCJNTDcr1ZXPof3+fO1l6HmhT
-QBSqC4IZL8XqANltDT4tCQDD8L9+ckWJD8MP3wPkPUGZId2gLu++hrb9YfiP2upq
-N/f3P7l5Fcisw1iwQC4+DGMTyfcCAwEAAaNpMGcwHQYDVR0OBBYEFGuiGk8HLpG2
-MyA24/J+GwxT32ikMB8GA1UdIwQYMBaAFEqpppD/6NP6Kk8sl7up6SnU3/OLMBoG
-A1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB
-CwUAA4IBAQB8yfY0edAgq2KnJNWyl8NpHNfqtM27+/LR2V8OxVwxV1hc4ZilczLu
-CXeqP9uqBVjcck6fvLrjy4LhSG0V05j51UMJ1FjFVTBuhlrDcd3j8848yWrmyz8z
-vPYYY2vIN9d1NsBgufULwliBT4UJchsYE8xT5ayAzGHKCTlzHGHMTPzYjwac8nbT
-nd2u+6h0OQOJn6K4v+RfXtN4EA8ZUrYxUkqHNS3cFB5sxH7JQGi25XJc5MfxyCwY
-YOukxbN85ew861N6oVd+W+nGJu8WOLU88/uvCv+dLhnAlnnIOLqvmrD5m7gFsFO9
-Z7Xz/U1SbNipWy9OLOhqq2Ja59j8p9e5
+ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDMxN1oYDzIwNjkxMjE0MTcwMzE3WjAYMRYw
+FAYDVQQDEw1lbGFzdGljc2VhcmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2EkPfvE3ZNMjHCAQZhpImoXBCIN6KavvJSbVHRtLzAXB4wxige+vFQWb
+4umqPeEeVH7FvrsRqn24tUgGIkag9p9AOwYxfcT3vwNqcK/EztIlYFs72pmYg7Ez
+s6+qLc/YSLOT3aMoHKDHE93z1jYIDGccyjGbv9NsdgCbLHD0TQuqm+7pKy1MZoJm
+0qn4KYw4kXakVNWlxm5GIwr8uqU/w4phrikcOOWqRzsxByoQajypLOA4eD/uWnI2
+zGyPQy7Bkxojiy1ss0CVlrl8fJgcjC4PONpm1ibUSX3SoZ8PopPThR6gvvwoQolR
+rYu4+D+rsX7q/ldA6vBOiHBD8r4QoQIDAQABo2MwYTAdBgNVHQ4EFgQUSlIMCYYd
+e72A0rUqaCkjVPkGPIwwHwYDVR0jBBgwFoAUGu0mDnvDRnBdNBG8DxwPdWArB0kw
+FAYDVR0RBA0wC4IJbG9jYWxob3N0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQAD
+ggEBAImbzBVAEjiLRsNDLP7QAl0k7lVmfQRFz5G95ZTAUSUgbqqymDvry47yInFF
+3o12TuI1GxK5zHzi+qzpJLyrnGwGK5JBR+VGxIBBKFVcFh1WNGKV6kSO/zBzO7PO
+4Jw4G7By/ImWvS0RBhBUQ9XbQZN3WcVkVVV8UQw5Y7JoKtM+fzyEKXKRCTsvgH+h
+3+fUBgqwal2Mz4KPH57Jrtk209dtn7tnQxHTNLo0niHyEcfrpuG3YFqTwekr+5FF
+FniIcYHPGjag1WzLIdyhe88FFpuav19mlCaxBACc7t97v+euSVUWnsKpy4dLydpv
+NxJiI9eWbJZ7f5VM7o64pm7U1cU=
-----END CERTIFICATE-----
diff --git a/packages/kbn-dev-utils/certs/elasticsearch.key b/packages/kbn-dev-utils/certs/elasticsearch.key
old mode 100755
new mode 100644
index 1013ce3971246..9ae4e314630d1
--- a/packages/kbn-dev-utils/certs/elasticsearch.key
+++ b/packages/kbn-dev-utils/certs/elasticsearch.key
@@ -1,27 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
-MIIEpAIBAAKCAQEAtb7yBXqbrBiZNmatfM1YoSUT54+SSKQK+I1+5aidarGAChak
-DADHYW92E/dQMVt7fzJ0lfigOBd4Lbm90hHrPa6rPH4ZdI1LJs7uW645RhA4/hd/
-kKPfPNdczK6djWdlNWwKEj2MAzPhxll1R3Th/ivhME3GsUJxVHS55zlEZFe4/ytJ
-B30BtgVRVh+Ml4tvbhqyCFvYq75zYka8yo8RcVYUY5cWFSghtGMIk1MNyvVlc+h/
-f587WXoeaFNAFKoLghkvxeoA2W0NPi0JAMPwv35yRYkPww/fA+Q9QZkh3aAu776G
-tv1h+I/a6mo39/c/uXkVyKzDWLBALj4MYxPJ9wIDAQABAoIBAQCb1ggrjn/gxo7I
-yK3FL0XplqNEkCR8SLxndtvyC+w+Schh3hv3dst+zlXOtOZ8C9cOr7KrzS2EKwuP
-GY6bi2XL0/NbwTwOZgCkXBahYfgWDV7w8DEfUoPd5UPa9XZ+gsOTVPolvcRKErhq
-nNYk2SHWEMXb5zSRVUlbg2LL0pzD88bIuKJX+FwPvWcQc2P4OdVTq77iedcl82zZ
-6PqTNqKMep7/odLQeBfX7OapOAviVnPYHe0TA114COOimR/pK8IA1OJymX5rgU7O
-Wh+uNBSxdHsTTYTkAvw8Bt5Q8n1WCpQwZoYU3xWuSlu7eJ7kcgdFOu9r9GjSXysT
-UYCd8s0BAoGBAPXPpCDRxjqF3/ToZ5x5dorKxxJyrmldzMJaUjqOv7y6kezbdBql
-n7p3AJ5UfYUW/N6pgQXaWF4MPSyj7ItHhwHjL+v0Manmi5gq8oA30fplhjUlPre7
-Lx4v7SEmH739EHrkZ2ClIQwY3wKuN8mZKgw6RseFgphczDmhHCqEbjW3AoGBAL1H
-fkl0RNdZ3nZg0u7MUVk8ytnqBsp7bNFhEs0zUl7ghu3NLaPt8qhirG638oMSCxqH
-FPeM3/DryokQAym+UHYNMwiBziEUB2CKMMj7S5YFFWIldCxFeImCO2EP+y3hmbTZ
-yjsznNrDzQtErZGP+JTRZcy9xF0oAfVt0G/O1Q3BAoGAa8bqINW5g6l1Q82uuEXt
-evdkB6uu21YcVE8D5Nb4LMjk+KRUKObbvQc2hzVmf7dPklVh0+4jdsEJBYyuR3dK
-M8KoHV3JdMQ4CrUx9JQFBjQDf0PgVvDEvQiogTNVEZlm42tIBHECp2o0RdmbblIw
-xIG8zPi2BRYTGWWRkvbT18sCgYA+c/B/XBW62LRGavwuPsw4nY5xCH7lIIRvMZB6
-lIyBMaRToneEt2ZxmN08SwWBqdpwDlIkvB7H54UUZGwmwdzaltBX5jyVPX6RpAck
-yYXPIi5EDAeg8+sptAbTp+pA4UdOHO5VSlpe9GwbY7XBabejotPsElFQS3sZ9/nm
-amByAQKBgQCJWghllys1qk76/6PmeVjwjaK9n8o+94LWhqODXlACmDRyse5dwpYb
-BIsMMZrNu1YsqDXlWpU7xNa6A8j4oa+EPnm/01PjdueAvMB/oE1woawM5tSsd8NQ
-zeQPDhxjDxzaO5l4oJLZg6FT7iQAprhYZjgb8m1vz0D2Xid0A3Kgpw==
+MIIEogIBAAKCAQEA2EkPfvE3ZNMjHCAQZhpImoXBCIN6KavvJSbVHRtLzAXB4wxi
+ge+vFQWb4umqPeEeVH7FvrsRqn24tUgGIkag9p9AOwYxfcT3vwNqcK/EztIlYFs7
+2pmYg7Ezs6+qLc/YSLOT3aMoHKDHE93z1jYIDGccyjGbv9NsdgCbLHD0TQuqm+7p
+Ky1MZoJm0qn4KYw4kXakVNWlxm5GIwr8uqU/w4phrikcOOWqRzsxByoQajypLOA4
+eD/uWnI2zGyPQy7Bkxojiy1ss0CVlrl8fJgcjC4PONpm1ibUSX3SoZ8PopPThR6g
+vvwoQolRrYu4+D+rsX7q/ldA6vBOiHBD8r4QoQIDAQABAoIBAB+s44YV0aUEfvnZ
+gE1TwBpRSGn0x2le8tEgFMoEe19P4Itd/vdEoQGVJrVevz38wDJjtpYuU3ICo5B5
+EdznNx+nRwLd71WaCSaCW45RT6Nyh2LLOcLUB9ARnZ7NNUEsVWKgWiF1iaRXr5Ar
+S1Ct7RPT7hV2mnbHgfTuNcuWZ1D5BUcqNczNoHsV6guFChiwTr7ZObnKj4qJLwdu
+ioYYWno4ZLgsk4SfW6DXUCvfKROfYdDd2rGu0NQ4QxT3Q98AsXlrlUITBQbpQEgy
+5GSTEh/4sRYj4NQZqncDpPgXm22kYdU7voBjt/zu66oq1W6kKQ4JwPmyc2SI0haa
+/pyCMtkCgYEA/y3vs59RvrM6xpT77lf7WigSBbIBQxeKs9RGNoN0Nn/eR0MlQAUG
+SmCkkEOcUGuVMnoo5Kc73IP/Q1+O4UGg7f1Gs8KeFPFQMm/wcSL7obvRWray1Bw6
+ohITJPqZYZrw3hmkOMxkLpvUydivN1Unm7BezjOa+T/+OaV3PyAYufsCgYEA2Psb
+S8OQhFiVbOKlMYOebvG+AnhAzJiSVus9R9NcViv20E61PRj2rfA398pYpZ8nxaQp
+cWGy+POZbkxRCprZ1GHkwWjaQysgeOCbJv8nQ2oh5C0ZCaGw6lfmi2mN097+Prmx
+QE8j8OKj3wVI6bniCF7vzwfG3c5cU73elLTAWRMCgYBoA/eDRlvx2ekJbU1MGDzy
+wQann6l4Ca6WIt8D9Y13caPPdIVIlUO9KauqyoR7G39TdgwZODnkZ0Gz2s3I8BGD
+MQyS1a/OZZcFGC/wTgw4HvD1gydd4qvbyHZZSnUfHiM0xUr1hAsKHKceJ980NNfS
+VJAwiUSQeQ9NvC7hYlnx5QKBgDxESsmZcRuBa0eKEC4Xi7rvBEK1WfI58nOX9TZs
++3mnzm7/XZGxzFp1nWYC2uptsWNQ/H3UkBxbtOMQ6XWTmytFYX9i+zSq1uMcJ5wG
+RMaRxQYWjJzDP1tnvM4+LDmL93w+oX/mO2pd2PxKAH2CtshybhNH6rGS7swHsboG
+FmLnAoGAYTnTcWD1qiwjbJR5ZdukAjIq39cGcf0YOVJCiaFS+5vTirbw04ARvNyM
+rxU8EpVN1sKC411pgNvlm6KZJHwihRRQoY+UI2fn78bHBH991QhlrTPO6TBZx7Aw
++hzyxqAiSBX65dQo0e4C15wZysQO/bdT5Def0+UTDR8j8ZgMAQg=
-----END RSA PRIVATE KEY-----
diff --git a/packages/kbn-dev-utils/certs/elasticsearch.p12 b/packages/kbn-dev-utils/certs/elasticsearch.p12
new file mode 100644
index 0000000000000..02a9183cd8a50
Binary files /dev/null and b/packages/kbn-dev-utils/certs/elasticsearch.p12 differ
diff --git a/packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12 b/packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12
new file mode 100644
index 0000000000000..3162982ac635a
Binary files /dev/null and b/packages/kbn-dev-utils/certs/elasticsearch_emptypassword.p12 differ
diff --git a/packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12 b/packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12
new file mode 100644
index 0000000000000..3a22a58d207df
Binary files /dev/null and b/packages/kbn-dev-utils/certs/elasticsearch_nopassword.p12 differ
diff --git a/packages/kbn-dev-utils/certs/kibana.crt b/packages/kbn-dev-utils/certs/kibana.crt
new file mode 100644
index 0000000000000..1c83be587bff9
--- /dev/null
+++ b/packages/kbn-dev-utils/certs/kibana.crt
@@ -0,0 +1,29 @@
+Bag Attributes
+ friendlyName: kibana
+ localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 32 32 33 30 33 39
+Key Attributes:
+Bag Attributes
+ friendlyName: kibana
+ localKeyID: 54 69 6D 65 20 31 35 37 37 34 36 36 32 32 33 30 33 39
+subject=/CN=kibana
+issuer=/CN=Elastic Certificate Tool Autogenerated CA
+-----BEGIN CERTIFICATE-----
+MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB
+CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu
+ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w
+DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ
+wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU
+FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q
+OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ
+s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU
+vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T
+BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz
+V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE
+DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/
+z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv
++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX
+TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy
+b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk
+cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O
+eOUsdwn1yDKHRxDHyA==
+-----END CERTIFICATE-----
diff --git a/packages/kbn-dev-utils/certs/kibana.key b/packages/kbn-dev-utils/certs/kibana.key
new file mode 100644
index 0000000000000..4a4e6b4cb8c36
--- /dev/null
+++ b/packages/kbn-dev-utils/certs/kibana.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAkMGGG0LW0QSieLjWXNviIEVPuzTS9tWXm0JRCDICu4wKjjK7
+Ftm+QwE8+NRXfhkBVBVGGI8KPj0I+rCR7ITiaP4DW99h68nbzCO2D73r5LM5LhBg
+n18UNw8NkUwAi4A9EDgpMB7590zH9JaJaFf3jxlmmQ2GWl4t2uQ3VovxxRaCcrIB
+dCVdn+l8ioSZBmhHybPaVl9uvcx/10LxjceARIpiaWAfIeCZw2DxSIkHao/rJdm/
+pEtYR/ZC+YAQDF4xVL3DZ1/j5sk48iaxqO+ZNJLvrKQX9WzMLkLd7kpjPk0CF5Lx
+Q5ktAzlREq6QORAtUwVBkc+qPyPPF8eDLne+9QIDAQABAoIBAHl9suxWYKz00te3
+alJtSZAEHDLm1tjL034/XnseXiTCGGnYMiWvgnwCIgZFUVlH61GCuV4LT3GFEHA2
+mYKE1PGBn5gQF8MpnAvtPPRhVgaQVUFQBYg86F59h8mWnC545sciG4+DsA/apUem
+wJSOn/u+Odni/AwEV0ALolZFBhl+0rccSr+6paJnzJ7QNiIn6EWbgb0n9WXqkhap
+TqoPclBHm0ObeBI6lNyfvBZ8HB3hyjWZInNCaAs9DnkNPh4evuttUn/KlOPOVn9r
+xz2UYsmVW6E+yPXUpSYkFQN9aaPF6alOz8PIfF8Wit7pmZMmInluGcwi/us9+ZTN
+8gNvpoECgYEA0KC7XEoXRsBTN4kPznkGftvj1dtgB35W/HxXNouArQQjCbLhqcsA
+jqaK0f+stYzSWZXGsKl9yQU9KA7u/wCHmLep70l7WsYYUKdkhWouK0HU5MeeLwB0
+N4ekQOQuQGqelqMo7IG2hQhTYD9PB4F3G0Sz1FgdObfuGPKfvNFVjckCgYEAsaAA
+IY/TpRBWeWZfyXrnkp3atOPzkdpjb6cfT8Kib9bIECXr7ULUxA5QANX05ofodhsW
+3+7iW5wicyZ1VNVEsPRL0aw7YUbNpBvob8faBUZ2KEdKQr42IfVOo7TQnvVXtumR
+UE+dNvWUL2PbL0wMxD1XbMSmOze/wF8X2CeyDc0CgYBQnLqol2xVBz1gaRJ1emgb
+HoXzfVemrZeY6cadKdwnfkC3n6n4fJsTg6CCMiOe5vHkca4bVvJmeSK/Vr3cRG0g
+gl8kOaVzVrXQfE2oC3YZes9zMvqZOLivODcsZ77DXy82D4dhk2FeF/B3cR7tTIYk
+QDCoLP/l7H8QnrdAMza2mQKBgDODwuX475ncviehUEB/26+DBo4V2ms/mj0kjAk2
+2qNy+DzuspjyHADsYbmMU+WUHxA51Q2HG7ET/E3HJpo+7BgiEecye1pADZ391hCt
+Nob3I4eU/W2T+uEoYvFJnIOthg3veYyAOolY+ewwmr4B4WX8oGFUOx3Lklo5ehHf
+mV01AoGBAI/c6OoHdcqQsZxKlxDNLyB2bTbowAcccoZIOjkC5fkkbsmMDLfScBfW
+Q4YYJsmJBdrWNvo7jCl17Mcc4Is3RlmHDrItRkaZj+ehqAN3ejrnPLdgYeW/5XDK
+e7yBj7oJd4oKZc59jVytdHvo5R8K0QohAv9gQEZ/tdypX+xWe+5E
+-----END RSA PRIVATE KEY-----
diff --git a/packages/kbn-dev-utils/certs/kibana.p12 b/packages/kbn-dev-utils/certs/kibana.p12
new file mode 100644
index 0000000000000..06bbd23881290
Binary files /dev/null and b/packages/kbn-dev-utils/certs/kibana.p12 differ
diff --git a/packages/kbn-dev-utils/src/certs.ts b/packages/kbn-dev-utils/src/certs.ts
index 0d340e4e8c906..f72e3ee547b5c 100644
--- a/packages/kbn-dev-utils/src/certs.ts
+++ b/packages/kbn-dev-utils/src/certs.ts
@@ -22,3 +22,14 @@ import { resolve } from 'path';
export const CA_CERT_PATH = resolve(__dirname, '../certs/ca.crt');
export const ES_KEY_PATH = resolve(__dirname, '../certs/elasticsearch.key');
export const ES_CERT_PATH = resolve(__dirname, '../certs/elasticsearch.crt');
+export const ES_P12_PATH = resolve(__dirname, '../certs/elasticsearch.p12');
+export const ES_P12_PASSWORD = 'storepass';
+export const ES_EMPTYPASSWORD_P12_PATH = resolve(
+ __dirname,
+ '../certs/elasticsearch_emptypassword.p12'
+);
+export const ES_NOPASSWORD_P12_PATH = resolve(__dirname, '../certs/elasticsearch_nopassword.p12');
+export const KBN_KEY_PATH = resolve(__dirname, '../certs/kibana.key');
+export const KBN_CERT_PATH = resolve(__dirname, '../certs/kibana.crt');
+export const KBN_P12_PATH = resolve(__dirname, '../certs/kibana.p12');
+export const KBN_P12_PASSWORD = 'storepass';
diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts
index dc12613cf2a9e..2fc29b71b262e 100644
--- a/packages/kbn-dev-utils/src/index.ts
+++ b/packages/kbn-dev-utils/src/index.ts
@@ -25,7 +25,19 @@ export {
ToolingLogCollectingWriter,
} from './tooling_log';
export { createAbsolutePathSerializer } from './serializers';
-export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs';
+export {
+ CA_CERT_PATH,
+ ES_KEY_PATH,
+ ES_CERT_PATH,
+ ES_P12_PATH,
+ ES_P12_PASSWORD,
+ ES_EMPTYPASSWORD_P12_PATH,
+ ES_NOPASSWORD_P12_PATH,
+ KBN_KEY_PATH,
+ KBN_CERT_PATH,
+ KBN_P12_PATH,
+ KBN_P12_PASSWORD,
+} from './certs';
export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run';
export { REPO_ROOT } from './repo_root';
export { KbnClient } from './kbn_client';
diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js
index 665f80e3802e3..ceb4a5b6aece1 100644
--- a/packages/kbn-es/src/cluster.js
+++ b/packages/kbn-es/src/cluster.js
@@ -35,7 +35,7 @@ const { createCliError } = require('./errors');
const { promisify } = require('util');
const treeKillAsync = promisify(require('tree-kill'));
const { parseSettings, SettingsFilter } = require('./settings');
-const { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils');
+const { CA_CERT_PATH, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils');
const readFile = util.promisify(fs.readFile);
// listen to data on stream until map returns anything but undefined
@@ -261,9 +261,9 @@ exports.Cluster = class Cluster {
const esArgs = [].concat(options.esArgs || []);
if (this._ssl) {
esArgs.push('xpack.security.http.ssl.enabled=true');
- esArgs.push(`xpack.security.http.ssl.key=${ES_KEY_PATH}`);
- esArgs.push(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`);
- esArgs.push(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`);
+ esArgs.push(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`);
+ esArgs.push(`xpack.security.http.ssl.keystore.type=PKCS12`);
+ esArgs.push(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`);
}
const args = parseSettings(extractConfigFiles(esArgs, installPath, { log: this._log }), {
diff --git a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js
index d3181a748ffbb..d374abe5db068 100644
--- a/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js
+++ b/packages/kbn-es/src/integration_tests/__fixtures__/es_bin.js
@@ -34,6 +34,8 @@ if (!start) {
let serverUrl;
const server = createServer(
{
+ // Note: the integration uses the ES_P12_PATH, but that keystore contains
+ // the same key/cert as ES_KEY_PATH and ES_CERT_PATH
key: ssl ? fs.readFileSync(ES_KEY_PATH) : undefined,
cert: ssl ? fs.readFileSync(ES_CERT_PATH) : undefined,
},
diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js
index dd570e27e3282..dfbc04477bd40 100644
--- a/packages/kbn-es/src/integration_tests/cluster.test.js
+++ b/packages/kbn-es/src/integration_tests/cluster.test.js
@@ -17,7 +17,7 @@
* under the License.
*/
-const { ToolingLog, CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } = require('@kbn/dev-utils');
+const { ToolingLog, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils');
const execa = require('execa');
const { Cluster } = require('../cluster');
const { installSource, installSnapshot, installArchive } = require('../install');
@@ -252,9 +252,9 @@ describe('#start(installPath)', () => {
const config = extractConfigFiles.mock.calls[0][0];
expect(config).toContain('xpack.security.http.ssl.enabled=true');
- expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`);
- expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`);
- expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`);
+ expect(config).toContain(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`);
+ expect(config).toContain(`xpack.security.http.ssl.keystore.type=PKCS12`);
+ expect(config).toContain(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`);
});
it(`doesn't setup SSL when disabled`, async () => {
@@ -319,9 +319,9 @@ describe('#run()', () => {
const config = extractConfigFiles.mock.calls[0][0];
expect(config).toContain('xpack.security.http.ssl.enabled=true');
- expect(config).toContain(`xpack.security.http.ssl.key=${ES_KEY_PATH}`);
- expect(config).toContain(`xpack.security.http.ssl.certificate=${ES_CERT_PATH}`);
- expect(config).toContain(`xpack.security.http.ssl.certificate_authorities=${CA_CERT_PATH}`);
+ expect(config).toContain(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`);
+ expect(config).toContain(`xpack.security.http.ssl.keystore.type=PKCS12`);
+ expect(config).toContain(`xpack.security.http.ssl.keystore.password=${ES_P12_PASSWORD}`);
});
it(`doesn't setup SSL when disabled`, async () => {
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index c3f3f2f477fdd..c806547595089 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -4500,6 +4500,14 @@ var certs_1 = __webpack_require__(422);
exports.CA_CERT_PATH = certs_1.CA_CERT_PATH;
exports.ES_KEY_PATH = certs_1.ES_KEY_PATH;
exports.ES_CERT_PATH = certs_1.ES_CERT_PATH;
+exports.ES_P12_PATH = certs_1.ES_P12_PATH;
+exports.ES_P12_PASSWORD = certs_1.ES_P12_PASSWORD;
+exports.ES_EMPTYPASSWORD_P12_PATH = certs_1.ES_EMPTYPASSWORD_P12_PATH;
+exports.ES_NOPASSWORD_P12_PATH = certs_1.ES_NOPASSWORD_P12_PATH;
+exports.KBN_KEY_PATH = certs_1.KBN_KEY_PATH;
+exports.KBN_CERT_PATH = certs_1.KBN_CERT_PATH;
+exports.KBN_P12_PATH = certs_1.KBN_P12_PATH;
+exports.KBN_P12_PASSWORD = certs_1.KBN_P12_PASSWORD;
var run_1 = __webpack_require__(423);
exports.run = run_1.run;
exports.createFailError = run_1.createFailError;
@@ -36986,6 +36994,14 @@ const path_1 = __webpack_require__(16);
exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt');
exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key');
exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt');
+exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12');
+exports.ES_P12_PASSWORD = 'storepass';
+exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12');
+exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12');
+exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key');
+exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt');
+exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12');
+exports.KBN_P12_PASSWORD = 'storepass';
/***/ }),
diff --git a/renovate.json5 b/renovate.json5
index f069e961c0f2b..560403046b0a5 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -625,6 +625,14 @@
'@types/node-fetch',
],
},
+ {
+ groupSlug: 'node-forge',
+ groupName: 'node-forge related packages',
+ packageNames: [
+ 'node-forge',
+ '@types/node-forge',
+ ],
+ },
{
groupSlug: 'nodemailer',
groupName: 'nodemailer related packages',
@@ -673,6 +681,14 @@
'@types/parse-link-header',
],
},
+ {
+ groupSlug: 'pegjs',
+ groupName: 'pegjs related packages',
+ packageNames: [
+ 'pegjs',
+ '@types/pegjs',
+ ],
+ },
{
groupSlug: 'pngjs',
groupName: 'pngjs related packages',
diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js
index 4472891e580fb..fc88f2657018f 100644
--- a/scripts/functional_tests.js
+++ b/scripts/functional_tests.js
@@ -17,12 +17,23 @@
* under the License.
*/
-require('../src/setup_node_env');
-require('@kbn/test').runTestsCli([
+// eslint-disable-next-line no-restricted-syntax
+const alwaysImportedTests = [
require.resolve('../test/functional/config.js'),
- require.resolve('../test/api_integration/config.js'),
require.resolve('../test/plugin_functional/config.js'),
- require.resolve('../test/interpreter_functional/config.ts'),
require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'),
+];
+// eslint-disable-next-line no-restricted-syntax
+const onlyNotInCoverageTests = [
+ require.resolve('../test/api_integration/config.js'),
+ require.resolve('../test/interpreter_functional/config.ts'),
require.resolve('../test/examples/config.js'),
+];
+
+require('../src/setup_node_env');
+require('@kbn/test').runTestsCli([
+ // eslint-disable-next-line no-restricted-syntax
+ ...alwaysImportedTests,
+ // eslint-disable-next-line no-restricted-syntax
+ ...(!!process.env.CODE_COVERAGE ? [] : onlyNotInCoverageTests),
]);
diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js
index 6b13d0dc32d3f..9cf5691b88399 100644
--- a/src/cli/serve/serve.js
+++ b/src/cli/serve/serve.js
@@ -102,6 +102,8 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
}
ensureNotDefined('server.ssl.certificate');
ensureNotDefined('server.ssl.key');
+ ensureNotDefined('server.ssl.keystore.path');
+ ensureNotDefined('server.ssl.truststore.path');
ensureNotDefined('elasticsearch.ssl.certificateAuthorities');
const elasticsearchHosts = (
@@ -119,6 +121,8 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
});
set('server.ssl.enabled', true);
+ // TODO: change this cert/key to KBN_CERT_PATH and KBN_KEY_PATH from '@kbn/dev-utils'; will require some work to avoid breaking
+ // functional tests. Once that is done, the existing test cert/key at DEV_SSL_CERT_PATH and DEV_SSL_KEY_PATH can be deleted.
set('server.ssl.certificate', DEV_SSL_CERT_PATH);
set('server.ssl.key', DEV_SSL_KEY_PATH);
set('elasticsearch.hosts', elasticsearchHosts);
diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md
index 1c78de966c46f..b70ac610f24a7 100644
--- a/src/core/MIGRATION.md
+++ b/src/core/MIGRATION.md
@@ -1194,6 +1194,7 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS
| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | |
| `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | |
| `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | |
+| `server.plugins.elasticsearch.createCluster(...)` | [`core.elasticsearch.createClient`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.createclient.md) | |
| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | |
| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | |
| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | |
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index 281778f9420dd..cafc7e5887e38 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -279,6 +279,37 @@ describe('PluginsService', () => {
expect((contracts.get('pluginA')! as any).setupValue).toEqual(1);
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(2);
});
+
+ describe('timeout', () => {
+ const flushPromises = () => new Promise(resolve => setImmediate(resolve));
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('throws timeout error if "setup" was not completed in 30 sec.', async () => {
+ mockPluginInitializers.set(
+ 'pluginA',
+ jest.fn(() => ({
+ setup: jest.fn(() => new Promise(i => i)),
+ start: jest.fn(() => ({ value: 1 })),
+ stop: jest.fn(),
+ }))
+ );
+ const pluginsService = new PluginsService(mockCoreContext, plugins);
+ const promise = pluginsService.setup(mockSetupDeps);
+
+ jest.runAllTimers(); // load plugin bundles
+ await flushPromises();
+ jest.runAllTimers(); // setup plugins
+
+ await expect(promise).rejects.toMatchInlineSnapshot(
+ `[Error: Setup lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]`
+ );
+ });
+ });
});
describe('#start()', () => {
@@ -331,6 +362,34 @@ describe('PluginsService', () => {
expect((contracts.get('pluginA')! as any).startValue).toEqual(2);
expect((contracts.get('pluginB')! as any).pluginAPlusB).toEqual(3);
});
+ describe('timeout', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('throws timeout error if "start" was not completed in 30 sec.', async () => {
+ mockPluginInitializers.set(
+ 'pluginA',
+ jest.fn(() => ({
+ setup: jest.fn(() => ({ value: 1 })),
+ start: jest.fn(() => new Promise(i => i)),
+ stop: jest.fn(),
+ }))
+ );
+ const pluginsService = new PluginsService(mockCoreContext, plugins);
+ await pluginsService.setup(mockSetupDeps);
+
+ const promise = pluginsService.start(mockStartDeps);
+ jest.runAllTimers();
+
+ await expect(promise).rejects.toMatchInlineSnapshot(
+ `[Error: Start lifecycle of "pluginA" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]`
+ );
+ });
+ });
});
describe('#stop()', () => {
diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts
index c1939a3397647..8e1574d05baf8 100644
--- a/src/core/public/plugins/plugins_service.ts
+++ b/src/core/public/plugins/plugins_service.ts
@@ -28,7 +28,9 @@ import {
} from './plugin_context';
import { InternalCoreSetup, InternalCoreStart } from '../core_system';
import { InjectedPluginMetadata } from '../injected_metadata';
+import { withTimeout } from '../../utils';
+const Sec = 1000;
/** @internal */
export type PluginsServiceSetupDeps = InternalCoreSetup;
/** @internal */
@@ -110,13 +112,15 @@ export class PluginsService implements CoreService
);
- contracts.set(
- pluginName,
- await plugin.setup(
+ const contract = await withTimeout({
+ promise: plugin.setup(
createPluginSetupContext(this.coreContext, deps, plugin),
pluginDepContracts
- )
- );
+ ),
+ timeout: 30 * Sec,
+ errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`,
+ });
+ contracts.set(pluginName, contract);
this.satupPlugins.push(pluginName);
}
@@ -142,13 +146,15 @@ export class PluginsService implements CoreService
);
- contracts.set(
- pluginName,
- await plugin.start(
+ const contract = await withTimeout({
+ promise: plugin.start(
createPluginStartContext(this.coreContext, deps, plugin),
pluginDepContracts
- )
- );
+ ),
+ timeout: 30 * Sec,
+ errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`,
+ });
+ contracts.set(pluginName, contract);
}
// Expose start contracts
diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts
index b40dbdc1b6651..7851522ec899f 100644
--- a/src/core/server/config/deprecation/core_deprecations.test.ts
+++ b/src/core/server/config/deprecation/core_deprecations.test.ts
@@ -208,4 +208,35 @@ describe('core deprecations', () => {
).toEqual([`worker-src blob:`]);
});
});
+
+ describe('elasticsearchUsernameDeprecation', () => {
+ it('logs a warning if elasticsearch.username is set to "elastic"', () => {
+ const { messages } = applyCoreDeprecations({
+ elasticsearch: {
+ username: 'elastic',
+ },
+ });
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "Setting elasticsearch.username to \\"elastic\\" is deprecated. You should use the \\"kibana\\" user instead.",
+ ]
+ `);
+ });
+
+ it('does not log a warning if elasticsearch.username is set to something besides "elastic"', () => {
+ const { messages } = applyCoreDeprecations({
+ elasticsearch: {
+ username: 'otheruser',
+ },
+ });
+ expect(messages).toHaveLength(0);
+ });
+
+ it('does not log a warning if elasticsearch.username is unset', () => {
+ const { messages } = applyCoreDeprecations({
+ elasticsearch: {},
+ });
+ expect(messages).toHaveLength(0);
+ });
+ });
});
diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts
index 36fe95e05cb53..e3b66414ee163 100644
--- a/src/core/server/config/deprecation/core_deprecations.ts
+++ b/src/core/server/config/deprecation/core_deprecations.ts
@@ -91,6 +91,16 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
return settings;
};
+const elasticsearchUsernameDeprecation: ConfigDeprecation = (settings, _fromPath, log) => {
+ const username: string | undefined = get(settings, 'elasticsearch.username');
+ if (username === 'elastic') {
+ log(
+ `Setting elasticsearch.username to "elastic" is deprecated. You should use the "kibana" user instead.`
+ );
+ }
+ return settings;
+};
+
export const coreDeprecationProvider: ConfigDeprecationProvider = ({
unusedFromRoot,
renameFromRoot,
@@ -110,4 +120,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({
dataPathDeprecation,
rewriteBasePathDeprecation,
cspRulesDeprecation,
+ elasticsearchUsernameDeprecation,
];
diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts
index d43ab9d546ed2..2352677b8d3e0 100644
--- a/src/core/server/elasticsearch/cluster_client.ts
+++ b/src/core/server/elasticsearch/cluster_client.ts
@@ -89,15 +89,35 @@ export interface FakeRequest {
}
/**
- * Represents an Elasticsearch cluster API client and allows to call API on behalf
- * of the internal Kibana user and the actual user that is derived from the request
- * headers (via `asScoped(...)`).
+ * Represents an Elasticsearch cluster API client created by the platform.
+ * It allows to call API on behalf of the internal Kibana user and
+ * the actual user that is derived from the request headers (via `asScoped(...)`).
*
* See {@link ClusterClient}.
*
* @public
*/
-export type IClusterClient = Pick;
+export type IClusterClient = Pick;
+
+/**
+ * Represents an Elasticsearch cluster API client created by a plugin.
+ * It allows to call API on behalf of the internal Kibana user and
+ * the actual user that is derived from the request headers (via `asScoped(...)`).
+ *
+ * See {@link ClusterClient}.
+ *
+ * @public
+ */
+export type ICustomClusterClient = Pick;
+
+/**
+ A user credentials container.
+ * It accommodates the necessary auth credentials to impersonate the current user.
+ *
+ * @public
+ * See {@link KibanaRequest}.
+ */
+export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest;
/**
* {@inheritDoc IClusterClient}
@@ -174,7 +194,7 @@ export class ClusterClient implements IClusterClient {
* @param request - Request the `IScopedClusterClient` instance will be scoped to.
* Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform
*/
- public asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): IScopedClusterClient {
+ public asScoped(request?: ScopeableRequest): IScopedClusterClient {
// It'd have been quite expensive to create and configure client for every incoming
// request since it involves parsing of the config, reading of the SSL certificate and
// key files etc. Moreover scoped client needs two Elasticsearch JS clients at the same
diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
index 64fb41cb3e4e5..20c10459e0e8a 100644
--- a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
@@ -17,8 +17,6 @@
* under the License.
*/
-import { mockReadFileSync } from './elasticsearch_client_config.test.mocks';
-
import { duration } from 'moment';
import { loggingServiceMock } from '../logging/logging_service.mock';
import {
@@ -66,8 +64,6 @@ Object {
});
test('parses fully specified config', () => {
- mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
-
const elasticsearchConfig: ElasticsearchClientConfig = {
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
@@ -87,9 +83,9 @@ test('parses fully specified config', () => {
sniffInterval: 11223344,
ssl: {
verificationMode: 'certificate',
- certificateAuthorities: ['ca-path-1', 'ca-path-2'],
- certificate: 'certificate-path',
- key: 'key-path',
+ certificateAuthorities: ['content-of-ca-path-1', 'content-of-ca-path-2'],
+ certificate: 'content-of-certificate-path',
+ key: 'content-of-key-path',
keyPassphrase: 'key-pass',
alwaysPresentCertificate: true,
},
@@ -497,6 +493,7 @@ Object {
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
+ "ca": undefined,
"rejectUnauthorized": false,
},
}
@@ -541,6 +538,7 @@ Object {
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
+ "ca": undefined,
"checkServerIdentity": [Function],
"rejectUnauthorized": true,
},
@@ -581,6 +579,7 @@ Object {
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
+ "ca": undefined,
"rejectUnauthorized": true,
},
}
@@ -606,8 +605,6 @@ Object {
});
test('#ignoreCertAndKey = true', () => {
- mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
-
expect(
parseElasticsearchClientConfig(
{
@@ -620,9 +617,9 @@ Object {
requestHeadersWhitelist: [],
ssl: {
verificationMode: 'certificate',
- certificateAuthorities: ['ca-path'],
- certificate: 'certificate-path',
- key: 'key-path',
+ certificateAuthorities: ['content-of-ca-path'],
+ certificate: 'content-of-certificate-path',
+ key: 'content-of-key-path',
keyPassphrase: 'key-pass',
alwaysPresentCertificate: true,
},
diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.ts b/src/core/server/elasticsearch/elasticsearch_client_config.ts
index dcc09f711abbe..287d835c40351 100644
--- a/src/core/server/elasticsearch/elasticsearch_client_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_client_config.ts
@@ -18,7 +18,6 @@
*/
import { ConfigOptions } from 'elasticsearch';
-import { readFileSync } from 'fs';
import { cloneDeep } from 'lodash';
import { Duration } from 'moment';
import { checkServerIdentity } from 'tls';
@@ -165,18 +164,12 @@ export function parseElasticsearchClientConfig(
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
}
- const readFile = (file: string) => readFileSync(file, 'utf8');
- if (
- config.ssl.certificateAuthorities !== undefined &&
- config.ssl.certificateAuthorities.length > 0
- ) {
- esClientConfig.ssl.ca = config.ssl.certificateAuthorities.map(readFile);
- }
+ esClientConfig.ssl.ca = config.ssl.certificateAuthorities;
// Add client certificate and key if required by elasticsearch
if (!ignoreCertAndKey && config.ssl.certificate && config.ssl.key) {
- esClientConfig.ssl.cert = readFile(config.ssl.certificate);
- esClientConfig.ssl.key = readFile(config.ssl.key);
+ esClientConfig.ssl.cert = config.ssl.certificate;
+ esClientConfig.ssl.key = config.ssl.key;
esClientConfig.ssl.passphrase = config.ssl.keyPassphrase;
}
diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts
new file mode 100644
index 0000000000000..d908fdbfd2e80
--- /dev/null
+++ b/src/core/server/elasticsearch/elasticsearch_config.test.mocks.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const mockReadFileSync = jest.fn();
+jest.mock('fs', () => ({ readFileSync: mockReadFileSync }));
+
+export const mockReadPkcs12Keystore = jest.fn();
+export const mockReadPkcs12Truststore = jest.fn();
+jest.mock('../../utils', () => ({
+ readPkcs12Keystore: mockReadPkcs12Keystore,
+ readPkcs12Truststore: mockReadPkcs12Truststore,
+}));
diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts
index 1d919639abe5f..c0db7369b4b99 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts
@@ -17,10 +17,25 @@
* under the License.
*/
-import { ElasticsearchConfig, config } from './elasticsearch_config';
+import {
+ mockReadFileSync,
+ mockReadPkcs12Keystore,
+ mockReadPkcs12Truststore,
+} from './elasticsearch_config.test.mocks';
+
+import { ElasticsearchConfig, config, ElasticsearchConfigType } from './elasticsearch_config';
+import { loggingServiceMock } from '../mocks';
+import { Logger } from '../logging';
+
+const createElasticsearchConfig = (rawConfig: ElasticsearchConfigType, log?: Logger) => {
+ if (!log) {
+ log = loggingServiceMock.create().get('config');
+ }
+ return new ElasticsearchConfig(rawConfig, log);
+};
test('set correct defaults', () => {
- const configValue = new ElasticsearchConfig(config.schema.validate({}));
+ const configValue = createElasticsearchConfig(config.schema.validate({}));
expect(configValue).toMatchInlineSnapshot(`
ElasticsearchConfig {
"apiVersion": "master",
@@ -43,7 +58,10 @@ test('set correct defaults', () => {
"sniffOnStart": false,
"ssl": Object {
"alwaysPresentCertificate": false,
+ "certificate": undefined,
"certificateAuthorities": undefined,
+ "key": undefined,
+ "keyPassphrase": undefined,
"verificationMode": "full",
},
"username": undefined,
@@ -52,17 +70,17 @@ test('set correct defaults', () => {
});
test('#hosts accepts both string and array of strings', () => {
- let configValue = new ElasticsearchConfig(
+ let configValue = createElasticsearchConfig(
config.schema.validate({ hosts: 'http://some.host:1234' })
);
expect(configValue.hosts).toEqual(['http://some.host:1234']);
- configValue = new ElasticsearchConfig(
+ configValue = createElasticsearchConfig(
config.schema.validate({ hosts: ['http://some.host:1234'] })
);
expect(configValue.hosts).toEqual(['http://some.host:1234']);
- configValue = new ElasticsearchConfig(
+ configValue = createElasticsearchConfig(
config.schema.validate({
hosts: ['http://some.host:1234', 'https://some.another.host'],
})
@@ -71,17 +89,17 @@ test('#hosts accepts both string and array of strings', () => {
});
test('#requestHeadersWhitelist accepts both string and array of strings', () => {
- let configValue = new ElasticsearchConfig(
+ let configValue = createElasticsearchConfig(
config.schema.validate({ requestHeadersWhitelist: 'token' })
);
expect(configValue.requestHeadersWhitelist).toEqual(['token']);
- configValue = new ElasticsearchConfig(
+ configValue = createElasticsearchConfig(
config.schema.validate({ requestHeadersWhitelist: ['token'] })
);
expect(configValue.requestHeadersWhitelist).toEqual(['token']);
- configValue = new ElasticsearchConfig(
+ configValue = createElasticsearchConfig(
config.schema.validate({
requestHeadersWhitelist: ['token', 'X-Forwarded-Proto'],
})
@@ -89,23 +107,216 @@ test('#requestHeadersWhitelist accepts both string and array of strings', () =>
expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']);
});
-test('#ssl.certificateAuthorities accepts both string and array of strings', () => {
- let configValue = new ElasticsearchConfig(
- config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } })
- );
- expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']);
+describe('reads files', () => {
+ beforeEach(() => {
+ mockReadFileSync.mockReset();
+ mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
+ mockReadPkcs12Keystore.mockReset();
+ mockReadPkcs12Keystore.mockImplementation((path: string) => ({
+ key: `content-of-${path}.key`,
+ cert: `content-of-${path}.cert`,
+ ca: [`content-of-${path}.ca`],
+ }));
+ mockReadPkcs12Truststore.mockReset();
+ mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]);
+ });
- configValue = new ElasticsearchConfig(
- config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } })
- );
- expect(configValue.ssl.certificateAuthorities).toEqual(['some-path']);
+ it('reads certificate authorities when ssl.keystore.path is specified', () => {
+ const configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { keystore: { path: 'some-path' } } })
+ );
+ expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']);
+ });
- configValue = new ElasticsearchConfig(
- config.schema.validate({
- ssl: { certificateAuthorities: ['some-path', 'another-path'] },
- })
- );
- expect(configValue.ssl.certificateAuthorities).toEqual(['some-path', 'another-path']);
+ it('reads certificate authorities when ssl.truststore.path is specified', () => {
+ const configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { truststore: { path: 'some-path' } } })
+ );
+ expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
+ });
+
+ it('reads certificate authorities when ssl.certificateAuthorities is specified', () => {
+ let configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } })
+ );
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
+
+ mockReadFileSync.mockClear();
+ configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } })
+ );
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
+
+ mockReadFileSync.mockClear();
+ configValue = createElasticsearchConfig(
+ config.schema.validate({
+ ssl: { certificateAuthorities: ['some-path', 'another-path'] },
+ })
+ );
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
+ expect(configValue.ssl.certificateAuthorities).toEqual([
+ 'content-of-some-path',
+ 'content-of-another-path',
+ ]);
+ });
+
+ it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => {
+ const configValue = createElasticsearchConfig(
+ config.schema.validate({
+ ssl: {
+ keystore: { path: 'some-path' },
+ truststore: { path: 'another-path' },
+ certificateAuthorities: 'yet-another-path',
+ },
+ })
+ );
+ expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
+ expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.certificateAuthorities).toEqual([
+ 'content-of-some-path.ca',
+ 'content-of-another-path',
+ 'content-of-yet-another-path',
+ ]);
+ });
+
+ it('reads a private key and certificate when ssl.keystore.path is specified', () => {
+ const configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { keystore: { path: 'some-path' } } })
+ );
+ expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.key).toEqual('content-of-some-path.key');
+ expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert');
+ });
+
+ it('reads a private key when ssl.key is specified', () => {
+ const configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { key: 'some-path' } })
+ );
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.key).toEqual('content-of-some-path');
+ });
+
+ it('reads a certificate when ssl.certificate is specified', () => {
+ const configValue = createElasticsearchConfig(
+ config.schema.validate({ ssl: { certificate: 'some-path' } })
+ );
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.ssl.certificate).toEqual('content-of-some-path');
+ });
+});
+
+describe('throws when config is invalid', () => {
+ beforeAll(() => {
+ const realFs = jest.requireActual('fs');
+ mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
+ const utils = jest.requireActual('../../utils');
+ mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
+ utils.readPkcs12Keystore(path, password)
+ );
+ mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) =>
+ utils.readPkcs12Truststore(path, password)
+ );
+ });
+
+ it('throws if key is invalid', () => {
+ const value = { ssl: { key: '/invalid/key' } };
+ expect(() =>
+ createElasticsearchConfig(config.schema.validate(value))
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/key'"`
+ );
+ });
+
+ it('throws if certificate is invalid', () => {
+ const value = { ssl: { certificate: '/invalid/cert' } };
+ expect(() =>
+ createElasticsearchConfig(config.schema.validate(value))
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/cert'"`
+ );
+ });
+
+ it('throws if certificateAuthorities is invalid', () => {
+ const value = { ssl: { certificateAuthorities: '/invalid/ca' } };
+ expect(() =>
+ createElasticsearchConfig(config.schema.validate(value))
+ ).toThrowErrorMatchingInlineSnapshot(`"ENOENT: no such file or directory, open '/invalid/ca'"`);
+ });
+
+ it('throws if keystore path is invalid', () => {
+ const value = { ssl: { keystore: { path: '/invalid/keystore' } } };
+ expect(() =>
+ createElasticsearchConfig(config.schema.validate(value))
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/keystore'"`
+ );
+ });
+
+ it('throws if keystore does not contain a key or certificate', () => {
+ mockReadPkcs12Keystore.mockReturnValueOnce({});
+ const value = { ssl: { keystore: { path: 'some-path' } } };
+ expect(() =>
+ createElasticsearchConfig(config.schema.validate(value))
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Did not find key or certificate in Elasticsearch keystore."`
+ );
+ });
+
+ it('throws if truststore path is invalid', () => {
+ const value = { ssl: { keystore: { path: '/invalid/truststore' } } };
+ expect(() =>
+ createElasticsearchConfig(config.schema.validate(value))
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/truststore'"`
+ );
+ });
+
+ it('throws if key and keystore.path are both specified', () => {
+ const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } };
+ expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot(
+ `"[ssl]: cannot use [key] when [keystore.path] is specified"`
+ );
+ });
+
+ it('throws if certificate and keystore.path are both specified', () => {
+ const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } };
+ expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot(
+ `"[ssl]: cannot use [certificate] when [keystore.path] is specified"`
+ );
+ });
+});
+
+describe('logs warnings', () => {
+ let logger: ReturnType;
+ let log: Logger;
+
+ beforeAll(() => {
+ mockReadFileSync.mockResolvedValue('foo');
+ });
+
+ beforeEach(() => {
+ logger = loggingServiceMock.create();
+ log = logger.get('config');
+ });
+
+ it('warns if ssl.key is set and ssl.certificate is not', () => {
+ createElasticsearchConfig(config.schema.validate({ ssl: { key: 'some-path' } }), log);
+ expect(loggingServiceMock.collect(logger).warn[0][0]).toMatchInlineSnapshot(
+ `"Detected a key without a certificate; mutual TLS authentication is disabled."`
+ );
+ });
+
+ it('warns if ssl.certificate is set and ssl.key is not', () => {
+ createElasticsearchConfig(config.schema.validate({ ssl: { certificate: 'some-path' } }), log);
+ expect(loggingServiceMock.collect(logger).warn[0][0]).toMatchInlineSnapshot(
+ `"Detected a certificate without a key; mutual TLS authentication is disabled."`
+ );
+ });
});
test('#username throws if equal to "elastic", only while running from source', () => {
diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts
index 8d92b12ae4a77..815005f65c6e7 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.ts
@@ -19,6 +19,8 @@
import { schema, TypeOf } from '@kbn/config-schema';
import { Duration } from 'moment';
+import { readFileSync } from 'fs';
+import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils';
import { Logger } from '../logging';
const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
@@ -67,19 +69,39 @@ export const config = {
pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }),
startupTimeout: schema.duration({ defaultValue: '5s' }),
logQueries: schema.boolean({ defaultValue: false }),
- ssl: schema.object({
- verificationMode: schema.oneOf(
- [schema.literal('none'), schema.literal('certificate'), schema.literal('full')],
- { defaultValue: 'full' }
- ),
- certificateAuthorities: schema.maybe(
- schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })])
- ),
- certificate: schema.maybe(schema.string()),
- key: schema.maybe(schema.string()),
- keyPassphrase: schema.maybe(schema.string()),
- alwaysPresentCertificate: schema.boolean({ defaultValue: false }),
- }),
+ ssl: schema.object(
+ {
+ verificationMode: schema.oneOf(
+ [schema.literal('none'), schema.literal('certificate'), schema.literal('full')],
+ { defaultValue: 'full' }
+ ),
+ certificateAuthorities: schema.maybe(
+ schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })])
+ ),
+ certificate: schema.maybe(schema.string()),
+ key: schema.maybe(schema.string()),
+ keyPassphrase: schema.maybe(schema.string()),
+ keystore: schema.object({
+ path: schema.maybe(schema.string()),
+ password: schema.maybe(schema.string()),
+ }),
+ truststore: schema.object({
+ path: schema.maybe(schema.string()),
+ password: schema.maybe(schema.string()),
+ }),
+ alwaysPresentCertificate: schema.boolean({ defaultValue: false }),
+ },
+ {
+ validate: rawConfig => {
+ if (rawConfig.key && rawConfig.keystore.path) {
+ return 'cannot use [key] when [keystore.path] is specified';
+ }
+ if (rawConfig.certificate && rawConfig.keystore.path) {
+ return 'cannot use [certificate] when [keystore.path] is specified';
+ }
+ },
+ }
+ ),
apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }),
healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }),
ignoreVersionMismatch: schema.boolean({ defaultValue: false }),
@@ -173,7 +195,7 @@ export class ElasticsearchConfig {
*/
public readonly ssl: Pick<
SslConfigSchema,
- Exclude
+ Exclude
> & { certificateAuthorities?: string[] };
/**
@@ -183,7 +205,7 @@ export class ElasticsearchConfig {
*/
public readonly customHeaders: ElasticsearchConfigType['customHeaders'];
- constructor(rawConfig: ElasticsearchConfigType, log?: Logger) {
+ constructor(rawConfig: ElasticsearchConfigType, log: Logger) {
this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch;
this.apiVersion = rawConfig.apiVersion;
this.logQueries = rawConfig.logQueries;
@@ -202,24 +224,87 @@ export class ElasticsearchConfig {
this.password = rawConfig.password;
this.customHeaders = rawConfig.customHeaders;
- const certificateAuthorities = Array.isArray(rawConfig.ssl.certificateAuthorities)
- ? rawConfig.ssl.certificateAuthorities
- : typeof rawConfig.ssl.certificateAuthorities === 'string'
- ? [rawConfig.ssl.certificateAuthorities]
- : undefined;
+ const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl;
+ const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig);
+
+ if (key && !certificate) {
+ log.warn(`Detected a key without a certificate; mutual TLS authentication is disabled.`);
+ } else if (certificate && !key) {
+ log.warn(`Detected a certificate without a key; mutual TLS authentication is disabled.`);
+ }
this.ssl = {
- ...rawConfig.ssl,
+ alwaysPresentCertificate,
+ key,
+ keyPassphrase,
+ certificate,
certificateAuthorities,
+ verificationMode,
};
+ }
+}
+
+const readKeyAndCerts = (rawConfig: ElasticsearchConfigType) => {
+ let key: string | undefined;
+ let keyPassphrase: string | undefined;
+ let certificate: string | undefined;
+ let certificateAuthorities: string[] | undefined;
- if (this.username === 'elastic' && log !== undefined) {
- // logger is optional / not used during tests
- // TODO: logger can be removed when issue #40255 is resolved to support deprecations in NP config service
- log.warn(
- `Setting the elasticsearch username to "elastic" is deprecated. You should use the "kibana" user instead.`,
- { tags: ['deprecation'] }
- );
+ const addCAs = (ca: string[] | undefined) => {
+ if (ca && ca.length) {
+ certificateAuthorities = [...(certificateAuthorities || []), ...ca];
+ }
+ };
+
+ if (rawConfig.ssl.keystore?.path) {
+ const keystore = readPkcs12Keystore(
+ rawConfig.ssl.keystore.path,
+ rawConfig.ssl.keystore.password
+ );
+ if (!keystore.key && !keystore.cert) {
+ throw new Error(`Did not find key or certificate in Elasticsearch keystore.`);
+ }
+ key = keystore.key;
+ certificate = keystore.cert;
+ addCAs(keystore.ca);
+ } else {
+ if (rawConfig.ssl.key) {
+ key = readFile(rawConfig.ssl.key);
+ keyPassphrase = rawConfig.ssl.keyPassphrase;
+ }
+ if (rawConfig.ssl.certificate) {
+ certificate = readFile(rawConfig.ssl.certificate);
}
}
-}
+
+ if (rawConfig.ssl.truststore?.path) {
+ const ca = readPkcs12Truststore(
+ rawConfig.ssl.truststore.path,
+ rawConfig.ssl.truststore.password
+ );
+ addCAs(ca);
+ }
+
+ const ca = rawConfig.ssl.certificateAuthorities;
+ if (ca) {
+ const parsed: string[] = [];
+ const paths = Array.isArray(ca) ? ca : [ca];
+ if (paths.length > 0) {
+ for (const path of paths) {
+ parsed.push(readFile(path));
+ }
+ addCAs(parsed);
+ }
+ }
+
+ return {
+ key,
+ keyPassphrase,
+ certificate,
+ certificateAuthorities,
+ };
+};
+
+const readFile = (file: string) => {
+ return readFileSync(file, 'utf8');
+};
diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts
index d935d1a66eccf..1b52f22c4da09 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts
@@ -18,33 +18,67 @@
*/
import { BehaviorSubject } from 'rxjs';
-import { IClusterClient } from './cluster_client';
+import { IClusterClient, ICustomClusterClient } from './cluster_client';
import { IScopedClusterClient } from './scoped_cluster_client';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
-import { InternalElasticsearchServiceSetup } from './types';
+import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup } from './types';
const createScopedClusterClientMock = (): jest.Mocked => ({
callAsInternalUser: jest.fn(),
callAsCurrentUser: jest.fn(),
});
-const createClusterClientMock = (): jest.Mocked => ({
- callAsInternalUser: jest.fn(),
- asScoped: jest.fn().mockImplementation(createScopedClusterClientMock),
+const createCustomClusterClientMock = (): jest.Mocked => ({
+ ...createClusterClientMock(),
close: jest.fn(),
});
+function createClusterClientMock() {
+ const client: jest.Mocked = {
+ callAsInternalUser: jest.fn(),
+ asScoped: jest.fn(),
+ };
+ client.asScoped.mockReturnValue(createScopedClusterClientMock());
+ return client;
+}
+
+type MockedElasticSearchServiceSetup = jest.Mocked<
+ ElasticsearchServiceSetup & {
+ adminClient: jest.Mocked;
+ dataClient: jest.Mocked;
+ }
+>;
+
const createSetupContractMock = () => {
- const setupContract: jest.Mocked = {
+ const setupContract: MockedElasticSearchServiceSetup = {
+ createClient: jest.fn(),
+ adminClient: createClusterClientMock(),
+ dataClient: createClusterClientMock(),
+ };
+ setupContract.createClient.mockReturnValue(createCustomClusterClientMock());
+ setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock());
+ setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock());
+ return setupContract;
+};
+
+type MockedInternalElasticSearchServiceSetup = jest.Mocked<
+ InternalElasticsearchServiceSetup & {
+ adminClient: jest.Mocked;
+ dataClient: jest.Mocked;
+ }
+>;
+const createInternalSetupContractMock = () => {
+ const setupContract: MockedInternalElasticSearchServiceSetup = {
+ ...createSetupContractMock(),
legacy: {
config$: new BehaviorSubject({} as ElasticsearchConfig),
},
-
- createClient: jest.fn().mockImplementation(createClusterClientMock),
- adminClient$: new BehaviorSubject((createClusterClientMock() as unknown) as IClusterClient),
- dataClient$: new BehaviorSubject((createClusterClientMock() as unknown) as IClusterClient),
+ adminClient$: new BehaviorSubject(createClusterClientMock()),
+ dataClient$: new BehaviorSubject(createClusterClientMock()),
};
+ setupContract.adminClient.asScoped.mockReturnValue(createScopedClusterClientMock());
+ setupContract.dataClient.asScoped.mockReturnValue(createScopedClusterClientMock());
return setupContract;
};
@@ -55,14 +89,16 @@ const createMock = () => {
start: jest.fn(),
stop: jest.fn(),
};
- mocked.setup.mockResolvedValue(createSetupContractMock());
+ mocked.setup.mockResolvedValue(createInternalSetupContractMock());
mocked.stop.mockResolvedValue();
return mocked;
};
export const elasticsearchServiceMock = {
create: createMock,
- createSetupContract: createSetupContractMock,
+ createInternalSetup: createInternalSetupContractMock,
+ createSetup: createSetupContractMock,
createClusterClient: createClusterClientMock,
+ createCustomClusterClient: createCustomClusterClientMock,
createScopedClusterClient: createScopedClusterClientMock,
};
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 6c4a1f263bc71..9f694ac1c46da 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -30,6 +30,7 @@ import { loggingServiceMock } from '../logging/logging_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
+import { elasticsearchServiceMock } from './elasticsearch_service.mock';
let elasticsearchService: ElasticsearchService;
const configService = configServiceMock.create();
@@ -69,6 +70,27 @@ describe('#setup', () => {
);
});
+ it('returns data and admin client as a part of the contract', async () => {
+ const mockAdminClusterClientInstance = elasticsearchServiceMock.createClusterClient();
+ const mockDataClusterClientInstance = elasticsearchServiceMock.createClusterClient();
+ MockClusterClient.mockImplementationOnce(
+ () => mockAdminClusterClientInstance
+ ).mockImplementationOnce(() => mockDataClusterClientInstance);
+
+ const setupContract = await elasticsearchService.setup(deps);
+
+ const adminClient = setupContract.adminClient;
+ const dataClient = setupContract.dataClient;
+
+ expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0);
+ await adminClient.callAsInternalUser('any');
+ expect(mockAdminClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1);
+
+ expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0);
+ await dataClient.callAsInternalUser('any');
+ expect(mockDataClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1);
+ });
+
it('returns data and admin client observables as a part of the contract', async () => {
const mockAdminClusterClientInstance = { close: jest.fn() };
const mockDataClusterClientInstance = { close: jest.fn() };
@@ -174,7 +196,11 @@ Object {
undefined,
],
"ssl": Object {
+ "alwaysPresentCertificate": undefined,
+ "certificate": undefined,
"certificateAuthorities": undefined,
+ "key": undefined,
+ "keyPassphrase": undefined,
"verificationMode": "none",
},
}
diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts
index be0a817c54146..de32e7f6cf225 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.ts
@@ -18,17 +18,18 @@
*/
import { ConnectableObservable, Observable, Subscription } from 'rxjs';
-import { filter, first, map, publishReplay, switchMap } from 'rxjs/operators';
+import { filter, first, map, publishReplay, switchMap, take } from 'rxjs/operators';
import { CoreService } from '../../types';
import { merge } from '../../utils';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
-import { ClusterClient } from './cluster_client';
+import { ClusterClient, ScopeableRequest } from './cluster_client';
import { ElasticsearchClientConfig } from './elasticsearch_client_config';
import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config';
import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/';
import { InternalElasticsearchServiceSetup } from './types';
+import { CallAPIOptions } from './api_types';
/** @internal */
interface CoreClusterClients {
@@ -94,11 +95,67 @@ export class ElasticsearchService implements CoreService clients.adminClient));
+ const dataClient$ = clients$.pipe(map(clients => clients.dataClient));
+
+ const adminClient = {
+ async callAsInternalUser(
+ endpoint: string,
+ clientParams: Record = {},
+ options?: CallAPIOptions
+ ) {
+ const client = await adminClient$.pipe(take(1)).toPromise();
+ return await client.callAsInternalUser(endpoint, clientParams, options);
+ },
+ asScoped(request: ScopeableRequest) {
+ return {
+ callAsInternalUser: adminClient.callAsInternalUser,
+ async callAsCurrentUser(
+ endpoint: string,
+ clientParams: Record = {},
+ options?: CallAPIOptions
+ ) {
+ const client = await adminClient$.pipe(take(1)).toPromise();
+ return await client
+ .asScoped(request)
+ .callAsCurrentUser(endpoint, clientParams, options);
+ },
+ };
+ },
+ };
+ const dataClient = {
+ async callAsInternalUser(
+ endpoint: string,
+ clientParams: Record = {},
+ options?: CallAPIOptions
+ ) {
+ const client = await dataClient$.pipe(take(1)).toPromise();
+ return await client.callAsInternalUser(endpoint, clientParams, options);
+ },
+ asScoped(request: ScopeableRequest) {
+ return {
+ callAsInternalUser: dataClient.callAsInternalUser,
+ async callAsCurrentUser(
+ endpoint: string,
+ clientParams: Record = {},
+ options?: CallAPIOptions
+ ) {
+ const client = await dataClient$.pipe(take(1)).toPromise();
+ return await client
+ .asScoped(request)
+ .callAsCurrentUser(endpoint, clientParams, options);
+ },
+ };
+ },
+ };
+
return {
legacy: { config$: clients$.pipe(map(clients => clients.config)) },
- adminClient$: clients$.pipe(map(clients => clients.adminClient)),
- dataClient$: clients$.pipe(map(clients => clients.dataClient)),
+ adminClient$,
+ dataClient$,
+ adminClient,
+ dataClient,
createClient: (type: string, clientConfig: Partial = {}) => {
const finalConfig = merge({}, config, clientConfig);
diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts
index 1f99f86d9887b..5d64fadfaa184 100644
--- a/src/core/server/elasticsearch/index.ts
+++ b/src/core/server/elasticsearch/index.ts
@@ -18,7 +18,13 @@
*/
export { ElasticsearchService } from './elasticsearch_service';
-export { IClusterClient, ClusterClient, FakeRequest } from './cluster_client';
+export {
+ ClusterClient,
+ FakeRequest,
+ IClusterClient,
+ ICustomClusterClient,
+ ScopeableRequest,
+} from './cluster_client';
export { IScopedClusterClient, ScopedClusterClient, Headers } from './scoped_cluster_client';
export { ElasticsearchClientConfig } from './elasticsearch_client_config';
export { config } from './elasticsearch_config';
diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts
index 505b57c7c9e8e..22340bf3f2fc6 100644
--- a/src/core/server/elasticsearch/types.ts
+++ b/src/core/server/elasticsearch/types.ts
@@ -20,7 +20,7 @@
import { Observable } from 'rxjs';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchClientConfig } from './elasticsearch_client_config';
-import { IClusterClient } from './cluster_client';
+import { IClusterClient, ICustomClusterClient } from './cluster_client';
/**
* @public
@@ -46,29 +46,29 @@ export interface ElasticsearchServiceSetup {
readonly createClient: (
type: string,
clientConfig?: Partial
- ) => IClusterClient;
+ ) => ICustomClusterClient;
/**
- * Observable of clients for the `admin` cluster. Observable emits when Elasticsearch config changes on the Kibana
- * server. See {@link IClusterClient}.
+ * A client for the `admin` cluster. All Elasticsearch config value changes are processed under the hood.
+ * See {@link IClusterClient}.
*
- * @exmaple
+ * @example
* ```js
- * const client = await elasticsearch.adminClient$.pipe(take(1)).toPromise();
+ * const client = core.elasticsearch.adminClient;
* ```
*/
- readonly adminClient$: Observable;
+ readonly adminClient: IClusterClient;
/**
- * Observable of clients for the `data` cluster. Observable emits when Elasticsearch config changes on the Kibana
- * server. See {@link IClusterClient}.
+ * A client for the `data` cluster. All Elasticsearch config value changes are processed under the hood.
+ * See {@link IClusterClient}.
*
- * @exmaple
+ * @example
* ```js
- * const client = await elasticsearch.dataClient$.pipe(take(1)).toPromise();
+ * const client = core.elasticsearch.dataClient;
* ```
*/
- readonly dataClient$: Observable;
+ readonly dataClient: IClusterClient;
}
/** @internal */
@@ -77,4 +77,7 @@ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceS
readonly legacy: {
readonly config$: Observable;
};
+
+ readonly adminClient$: Observable;
+ readonly dataClient$: Observable;
}
diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap
index 8856eb95ba722..28933a035c870 100644
--- a/src/core/server/http/__snapshots__/http_config.test.ts.snap
+++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap
@@ -67,10 +67,12 @@ Object {
],
"clientAuthentication": "none",
"enabled": false,
+ "keystore": Object {},
"supportedProtocols": Array [
"TLSv1.1",
"TLSv1.2",
],
+ "truststore": Object {},
},
"xsrf": Object {
"disableProtection": false,
@@ -87,24 +89,6 @@ exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = `
exports[`throws if invalid hostname 1`] = `"[host]: value is [asdf$%^] but it must be a valid hostname (see RFC 1123)."`;
-exports[`with TLS should accept known protocols\` 1`] = `
-"[ssl.supportedProtocols.0]: types that failed validation:
-- [ssl.supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500]
-- [ssl.supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500]
-- [ssl.supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]"
-`;
-
-exports[`with TLS should accept known protocols\` 2`] = `
-"[ssl.supportedProtocols.3]: types that failed validation:
-- [ssl.supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500]
-- [ssl.supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500]
-- [ssl.supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]"
-`;
-
-exports[`with TLS throws if TLS is enabled but \`certificate\` is not specified 1`] = `"[ssl]: must specify [certificate] and [key] when ssl is enabled"`;
-
-exports[`with TLS throws if TLS is enabled but \`key\` is not specified 1`] = `"[ssl]: must specify [certificate] and [key] when ssl is enabled"`;
-
exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`;
exports[`with compression accepts valid referrer whitelist 1`] = `
diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts
index 3dc5fa48bc366..7ac707b0f3d83 100644
--- a/src/core/server/http/http_config.test.ts
+++ b/src/core/server/http/http_config.test.ts
@@ -18,7 +18,7 @@
*/
import uuid from 'uuid';
-import { config, HttpConfig } from '.';
+import { config } from '.';
const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost'];
const invalidHostname = 'asdf$%^';
@@ -108,28 +108,6 @@ test('throws if xsrf.whitelist element does not start with a slash', () => {
});
describe('with TLS', () => {
- test('throws if TLS is enabled but `key` is not specified', () => {
- const httpSchema = config.schema;
- const obj = {
- ssl: {
- certificate: '/path/to/certificate',
- enabled: true,
- },
- };
- expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
- });
-
- test('throws if TLS is enabled but `certificate` is not specified', () => {
- const httpSchema = config.schema;
- const obj = {
- ssl: {
- enabled: true,
- key: '/path/to/key',
- },
- };
- expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
- });
-
test('throws if TLS is enabled but `redirectHttpFromPort` is equal to `port`', () => {
const httpSchema = config.schema;
const obj = {
@@ -143,189 +121,16 @@ describe('with TLS', () => {
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
+});
- test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => {
- const httpSchema = config.schema;
- const obj = {
- port: 1234,
- ssl: {
- enabled: false,
- clientAuthentication: 'optional',
- },
- };
- expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
- `"[ssl]: must enable ssl to use [clientAuthentication]"`
- );
- });
-
- test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => {
- const httpSchema = config.schema;
- const obj = {
- port: 1234,
- ssl: {
- enabled: false,
- clientAuthentication: 'required',
- },
- };
- expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
- `"[ssl]: must enable ssl to use [clientAuthentication]"`
- );
- });
-
- test('can specify `none` for [clientAuthentication] if ssl is not enabled', () => {
- const obj = {
- ssl: {
- enabled: false,
- clientAuthentication: 'none',
- },
- };
-
- const configValue = config.schema.validate(obj);
- expect(configValue.ssl.clientAuthentication).toBe('none');
- });
-
- test('can specify single `certificateAuthority` as a string', () => {
- const obj = {
- ssl: {
- certificate: '/path/to/certificate',
- certificateAuthorities: '/authority/',
- enabled: true,
- key: '/path/to/key',
- },
- };
-
- const configValue = config.schema.validate(obj);
- expect(configValue.ssl.certificateAuthorities).toBe('/authority/');
- });
-
- test('can specify socket timeouts', () => {
- const obj = {
- keepaliveTimeout: 1e5,
- socketTimeout: 5e5,
- };
- const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj);
- expect(keepaliveTimeout).toBe(1e5);
- expect(socketTimeout).toBe(5e5);
- });
-
- test('can specify several `certificateAuthorities`', () => {
- const obj = {
- ssl: {
- certificate: '/path/to/certificate',
- certificateAuthorities: ['/authority/1', '/authority/2'],
- enabled: true,
- key: '/path/to/key',
- },
- };
-
- const configValue = config.schema.validate(obj);
- expect(configValue.ssl.certificateAuthorities).toEqual(['/authority/1', '/authority/2']);
- });
-
- test('accepts known protocols`', () => {
- const httpSchema = config.schema;
- const singleKnownProtocol = {
- ssl: {
- certificate: '/path/to/certificate',
- enabled: true,
- key: '/path/to/key',
- supportedProtocols: ['TLSv1'],
- },
- };
-
- const allKnownProtocols = {
- ssl: {
- certificate: '/path/to/certificate',
- enabled: true,
- key: '/path/to/key',
- supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'],
- },
- };
-
- const singleKnownProtocolConfig = httpSchema.validate(singleKnownProtocol);
- expect(singleKnownProtocolConfig.ssl.supportedProtocols).toEqual(['TLSv1']);
-
- const allKnownProtocolsConfig = httpSchema.validate(allKnownProtocols);
- expect(allKnownProtocolsConfig.ssl.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']);
- });
-
- test('should accept known protocols`', () => {
- const httpSchema = config.schema;
-
- const singleUnknownProtocol = {
- ssl: {
- certificate: '/path/to/certificate',
- enabled: true,
- key: '/path/to/key',
- supportedProtocols: ['SOMEv100500'],
- },
- };
-
- const allKnownWithOneUnknownProtocols = {
- ssl: {
- certificate: '/path/to/certificate',
- enabled: true,
- key: '/path/to/key',
- supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'],
- },
- };
-
- expect(() => httpSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingSnapshot();
- expect(() =>
- httpSchema.validate(allKnownWithOneUnknownProtocols)
- ).toThrowErrorMatchingSnapshot();
- });
-
- test('HttpConfig instance should properly interpret `none` client authentication', () => {
- const httpConfig = new HttpConfig(
- config.schema.validate({
- ssl: {
- enabled: true,
- key: 'some-key-path',
- certificate: 'some-certificate-path',
- clientAuthentication: 'none',
- },
- }),
- {} as any
- );
-
- expect(httpConfig.ssl.requestCert).toBe(false);
- expect(httpConfig.ssl.rejectUnauthorized).toBe(false);
- });
-
- test('HttpConfig instance should properly interpret `optional` client authentication', () => {
- const httpConfig = new HttpConfig(
- config.schema.validate({
- ssl: {
- enabled: true,
- key: 'some-key-path',
- certificate: 'some-certificate-path',
- clientAuthentication: 'optional',
- },
- }),
- {} as any
- );
-
- expect(httpConfig.ssl.requestCert).toBe(true);
- expect(httpConfig.ssl.rejectUnauthorized).toBe(false);
- });
-
- test('HttpConfig instance should properly interpret `required` client authentication', () => {
- const httpConfig = new HttpConfig(
- config.schema.validate({
- ssl: {
- enabled: true,
- key: 'some-key-path',
- certificate: 'some-certificate-path',
- clientAuthentication: 'required',
- },
- }),
- {} as any
- );
-
- expect(httpConfig.ssl.requestCert).toBe(true);
- expect(httpConfig.ssl.rejectUnauthorized).toBe(true);
- });
+test('can specify socket timeouts', () => {
+ const obj = {
+ keepaliveTimeout: 1e5,
+ socketTimeout: 5e5,
+ };
+ const { keepaliveTimeout, socketTimeout } = config.schema.validate(obj);
+ expect(keepaliveTimeout).toBe(1e5);
+ expect(socketTimeout).toBe(5e5);
});
describe('with compression', () => {
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index df357aeaf2731..df7b4b5af4267 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -18,11 +18,7 @@
*/
import { Server } from 'http';
-
-jest.mock('fs', () => ({
- readFileSync: jest.fn(),
-}));
-
+import { readFileSync } from 'fs';
import supertest from 'supertest';
import { ByteSizeValue, schema } from '@kbn/config-schema';
@@ -39,6 +35,7 @@ import { loggingServiceMock } from '../logging/logging_service.mock';
import { HttpServer } from './http_server';
import { Readable } from 'stream';
import { RequestHandlerContext } from 'kibana/server';
+import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
const cookieOptions = {
name: 'sid',
@@ -55,6 +52,14 @@ const loggingService = loggingServiceMock.create();
const logger = loggingService.get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
+let certificate: string;
+let key: string;
+
+beforeAll(() => {
+ certificate = readFileSync(KBN_CERT_PATH, 'utf8');
+ key = readFileSync(KBN_KEY_PATH, 'utf8');
+});
+
beforeEach(() => {
config = {
host: '127.0.0.1',
@@ -68,10 +73,10 @@ beforeEach(() => {
...config,
ssl: {
enabled: true,
- certificate: '/certificate',
+ certificate,
cipherSuites: ['cipherSuite'],
getSecureOptions: () => 0,
- key: '/key',
+ key,
redirectHttpFromPort: config.port + 1,
},
} as HttpConfig;
diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts
index 22468a5b252f4..747fd5a10b168 100644
--- a/src/core/server/http/http_tools.ts
+++ b/src/core/server/http/http_tools.ts
@@ -17,7 +17,6 @@
* under the License.
*/
-import { readFileSync } from 'fs';
import { Lifecycle, Request, ResponseToolkit, Server, ServerOptions, Util } from 'hapi';
import Hoek from 'hoek';
import { ServerOptions as TLSOptions } from 'https';
@@ -66,14 +65,12 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = {
// TODO: Hapi types have a typo in `tls` property type definition: `https.RequestOptions` is used instead of
// `https.ServerOptions`, and `honorCipherOrder` isn't presented in `https.RequestOptions`.
const tlsOptions: TLSOptions = {
- ca:
- config.ssl.certificateAuthorities &&
- config.ssl.certificateAuthorities.map(caFilePath => readFileSync(caFilePath)),
- cert: readFileSync(ssl.certificate!),
+ ca: ssl.certificateAuthorities,
+ cert: ssl.certificate,
ciphers: config.ssl.cipherSuites.join(':'),
// We use the server's cipher order rather than the client's to prevent the BEAST attack.
honorCipherOrder: true,
- key: readFileSync(ssl.key!),
+ key: ssl.key,
passphrase: ssl.keyPassphrase,
secureOptions: ssl.getSecureOptions(),
requestCert: ssl.requestCert,
diff --git a/src/core/server/http/ssl_config.test.mocks.ts b/src/core/server/http/ssl_config.test.mocks.ts
new file mode 100644
index 0000000000000..ab98c3a27920c
--- /dev/null
+++ b/src/core/server/http/ssl_config.test.mocks.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const mockReadFileSync = jest.fn();
+jest.mock('fs', () => {
+ return { readFileSync: mockReadFileSync };
+});
+
+export const mockReadPkcs12Keystore = jest.fn();
+export const mockReadPkcs12Truststore = jest.fn();
+jest.mock('../../utils', () => ({
+ readPkcs12Keystore: mockReadPkcs12Keystore,
+ readPkcs12Truststore: mockReadPkcs12Truststore,
+}));
diff --git a/src/core/server/http/ssl_config.test.ts b/src/core/server/http/ssl_config.test.ts
new file mode 100644
index 0000000000000..738f86f7a69eb
--- /dev/null
+++ b/src/core/server/http/ssl_config.test.ts
@@ -0,0 +1,363 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ mockReadFileSync,
+ mockReadPkcs12Keystore,
+ mockReadPkcs12Truststore,
+} from './ssl_config.test.mocks';
+
+import { sslSchema, SslConfig } from './ssl_config';
+
+describe('#SslConfig', () => {
+ const createConfig = (obj: any) => new SslConfig(sslSchema.validate(obj));
+
+ beforeEach(() => {
+ mockReadFileSync.mockReset();
+ mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
+ mockReadPkcs12Keystore.mockReset();
+ mockReadPkcs12Keystore.mockImplementation((path: string) => ({
+ key: `content-of-${path}.key`,
+ cert: `content-of-${path}.cert`,
+ ca: [`content-of-${path}.ca`],
+ }));
+ mockReadPkcs12Truststore.mockReset();
+ mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]);
+ });
+
+ describe('throws when config is invalid', () => {
+ beforeEach(() => {
+ const realFs = jest.requireActual('fs');
+ mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
+ const utils = jest.requireActual('../../utils');
+ mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
+ utils.readPkcs12Keystore(path, password)
+ );
+ mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) =>
+ utils.readPkcs12Truststore(path, password)
+ );
+ });
+
+ test('throws if `key` is invalid', () => {
+ const obj = { key: '/invalid/key', certificate: '/valid/certificate' };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/key'"`
+ );
+ });
+
+ test('throws if `certificate` is invalid', () => {
+ mockReadFileSync.mockImplementationOnce((path: string) => `content-of-${path}`);
+ const obj = { key: '/valid/key', certificate: '/invalid/certificate' };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/certificate'"`
+ );
+ });
+
+ test('throws if `certificateAuthorities` is invalid', () => {
+ const obj = { certificateAuthorities: '/invalid/ca' };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/ca'"`
+ );
+ });
+
+ test('throws if `keystore.path` is invalid', () => {
+ const obj = { keystore: { path: '/invalid/keystore' } };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/keystore'"`
+ );
+ });
+
+ test('throws if `keystore.path` does not contain a private key', () => {
+ mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({
+ key: undefined,
+ certificate: 'foo',
+ }));
+ const obj = { keystore: { path: 'some-path' } };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"Did not find private key in keystore at [keystore.path]."`
+ );
+ });
+
+ test('throws if `keystore.path` does not contain a certificate', () => {
+ mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) => ({
+ key: 'foo',
+ certificate: undefined,
+ }));
+ const obj = { keystore: { path: 'some-path' } };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"Did not find certificate in keystore at [keystore.path]."`
+ );
+ });
+
+ test('throws if `truststore.path` is invalid', () => {
+ const obj = { truststore: { path: '/invalid/truststore' } };
+ expect(() => createConfig(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"ENOENT: no such file or directory, open '/invalid/truststore'"`
+ );
+ });
+ });
+
+ describe('reads files', () => {
+ it('reads certificate authorities when `keystore.path` is specified', () => {
+ const configValue = createConfig({ keystore: { path: 'some-path' } });
+ expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
+ expect(configValue.certificateAuthorities).toEqual(['content-of-some-path.ca']);
+ });
+
+ it('reads certificate authorities when `truststore.path` is specified', () => {
+ const configValue = createConfig({ truststore: { path: 'some-path' } });
+ expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
+ expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']);
+ });
+
+ it('reads certificate authorities when `certificateAuthorities` is specified', () => {
+ let configValue = createConfig({ certificateAuthorities: 'some-path' });
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']);
+
+ mockReadFileSync.mockClear();
+ configValue = createConfig({ certificateAuthorities: ['some-path'] });
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.certificateAuthorities).toEqual(['content-of-some-path']);
+
+ mockReadFileSync.mockClear();
+ configValue = createConfig({ certificateAuthorities: ['some-path', 'another-path'] });
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
+ expect(configValue.certificateAuthorities).toEqual([
+ 'content-of-some-path',
+ 'content-of-another-path',
+ ]);
+ });
+
+ it('reads certificate authorities when `keystore.path`, `truststore.path`, and `certificateAuthorities` are specified', () => {
+ const configValue = createConfig({
+ keystore: { path: 'some-path' },
+ truststore: { path: 'another-path' },
+ certificateAuthorities: 'yet-another-path',
+ });
+ expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
+ expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
+ expect(configValue.certificateAuthorities).toEqual([
+ 'content-of-some-path.ca',
+ 'content-of-another-path',
+ 'content-of-yet-another-path',
+ ]);
+ });
+
+ it('reads a private key and certificate when `keystore.path` is specified', () => {
+ const configValue = createConfig({ keystore: { path: 'some-path' } });
+ expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
+ expect(configValue.key).toEqual('content-of-some-path.key');
+ expect(configValue.certificate).toEqual('content-of-some-path.cert');
+ });
+
+ it('reads a private key and certificate when `key` and `certificate` are specified', () => {
+ const configValue = createConfig({ key: 'some-path', certificate: 'another-path' });
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
+ expect(configValue.key).toEqual('content-of-some-path');
+ expect(configValue.certificate).toEqual('content-of-another-path');
+ });
+ });
+});
+
+describe('#sslSchema', () => {
+ describe('throws when config is invalid', () => {
+ test('throws if both `key` and `keystore.path` are specified', () => {
+ const obj = {
+ key: '/path/to/key',
+ keystore: {
+ path: 'path/to/keystore',
+ },
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"cannot use [key] when [keystore.path] is specified"`
+ );
+ });
+
+ test('throws if both `certificate` and `keystore.path` are specified', () => {
+ const obj = {
+ certificate: '/path/to/certificate',
+ keystore: {
+ path: 'path/to/keystore',
+ },
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"cannot use [certificate] when [keystore.path] is specified"`
+ );
+ });
+
+ test('throws if TLS is enabled but `certificate` is specified and `key` is not', () => {
+ const obj = {
+ certificate: '/path/to/certificate',
+ enabled: true,
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"`
+ );
+ });
+
+ test('throws if TLS is enabled but `key` is specified and `certificate` is not', () => {
+ const obj = {
+ enabled: true,
+ key: '/path/to/key',
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"`
+ );
+ });
+
+ test('throws if TLS is enabled but `key`, `certificate`, and `keystore.path` are not specified', () => {
+ const obj = {
+ enabled: true,
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled"`
+ );
+ });
+
+ test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => {
+ const obj = {
+ enabled: false,
+ clientAuthentication: 'optional',
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"must enable ssl to use [clientAuthentication]"`
+ );
+ });
+
+ test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => {
+ const obj = {
+ enabled: false,
+ clientAuthentication: 'required',
+ };
+ expect(() => sslSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+ `"must enable ssl to use [clientAuthentication]"`
+ );
+ });
+ });
+
+ describe('#supportedProtocols', () => {
+ test('accepts known protocols`', () => {
+ const singleKnownProtocol = {
+ certificate: '/path/to/certificate',
+ enabled: true,
+ key: '/path/to/key',
+ supportedProtocols: ['TLSv1'],
+ };
+
+ const allKnownProtocols = {
+ certificate: '/path/to/certificate',
+ enabled: true,
+ key: '/path/to/key',
+ supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'],
+ };
+
+ const singleKnownProtocolConfig = sslSchema.validate(singleKnownProtocol);
+ expect(singleKnownProtocolConfig.supportedProtocols).toEqual(['TLSv1']);
+
+ const allKnownProtocolsConfig = sslSchema.validate(allKnownProtocols);
+ expect(allKnownProtocolsConfig.supportedProtocols).toEqual(['TLSv1', 'TLSv1.1', 'TLSv1.2']);
+ });
+
+ test('rejects unknown protocols`', () => {
+ const singleUnknownProtocol = {
+ certificate: '/path/to/certificate',
+ enabled: true,
+ key: '/path/to/key',
+ supportedProtocols: ['SOMEv100500'],
+ };
+
+ const allKnownWithOneUnknownProtocols = {
+ certificate: '/path/to/certificate',
+ enabled: true,
+ key: '/path/to/key',
+ supportedProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'SOMEv100500'],
+ };
+
+ expect(() => sslSchema.validate(singleUnknownProtocol)).toThrowErrorMatchingInlineSnapshot(`
+"[supportedProtocols.0]: types that failed validation:
+- [supportedProtocols.0.0]: expected value to equal [TLSv1] but got [SOMEv100500]
+- [supportedProtocols.0.1]: expected value to equal [TLSv1.1] but got [SOMEv100500]
+- [supportedProtocols.0.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]"
+`);
+ expect(() => sslSchema.validate(allKnownWithOneUnknownProtocols))
+ .toThrowErrorMatchingInlineSnapshot(`
+"[supportedProtocols.3]: types that failed validation:
+- [supportedProtocols.3.0]: expected value to equal [TLSv1] but got [SOMEv100500]
+- [supportedProtocols.3.1]: expected value to equal [TLSv1.1] but got [SOMEv100500]
+- [supportedProtocols.3.2]: expected value to equal [TLSv1.2] but got [SOMEv100500]"
+`);
+ });
+ });
+
+ describe('#clientAuthentication', () => {
+ test('can specify `none` client authentication when ssl is not enabled', () => {
+ const obj = {
+ enabled: false,
+ clientAuthentication: 'none',
+ };
+
+ const configValue = sslSchema.validate(obj);
+ expect(configValue.clientAuthentication).toBe('none');
+ });
+
+ test('should properly interpret `none` client authentication when ssl is enabled', () => {
+ const sslConfig = new SslConfig(
+ sslSchema.validate({
+ enabled: true,
+ key: 'some-key-path',
+ certificate: 'some-certificate-path',
+ clientAuthentication: 'none',
+ })
+ );
+
+ expect(sslConfig.requestCert).toBe(false);
+ expect(sslConfig.rejectUnauthorized).toBe(false);
+ });
+
+ test('should properly interpret `optional` client authentication when ssl is enabled', () => {
+ const sslConfig = new SslConfig(
+ sslSchema.validate({
+ enabled: true,
+ key: 'some-key-path',
+ certificate: 'some-certificate-path',
+ clientAuthentication: 'optional',
+ })
+ );
+
+ expect(sslConfig.requestCert).toBe(true);
+ expect(sslConfig.rejectUnauthorized).toBe(false);
+ });
+
+ test('should properly interpret `required` client authentication when ssl is enabled', () => {
+ const sslConfig = new SslConfig(
+ sslSchema.validate({
+ enabled: true,
+ key: 'some-key-path',
+ certificate: 'some-certificate-path',
+ clientAuthentication: 'required',
+ })
+ );
+
+ expect(sslConfig.requestCert).toBe(true);
+ expect(sslConfig.rejectUnauthorized).toBe(true);
+ });
+ });
+});
diff --git a/src/core/server/http/ssl_config.ts b/src/core/server/http/ssl_config.ts
index 55d6ebff93ce7..0096eeb092565 100644
--- a/src/core/server/http/ssl_config.ts
+++ b/src/core/server/http/ssl_config.ts
@@ -19,6 +19,8 @@
import { schema, TypeOf } from '@kbn/config-schema';
import crypto from 'crypto';
+import { readFileSync } from 'fs';
+import { readPkcs12Keystore, readPkcs12Truststore } from '../../utils';
// `crypto` type definitions doesn't currently include `crypto.constants`, see
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fa5baf1733f49cf26228a4e509914572c1b74adf/types/node/v6/index.d.ts#L3412
@@ -44,6 +46,14 @@ export const sslSchema = schema.object(
}),
key: schema.maybe(schema.string()),
keyPassphrase: schema.maybe(schema.string()),
+ keystore: schema.object({
+ path: schema.maybe(schema.string()),
+ password: schema.maybe(schema.string()),
+ }),
+ truststore: schema.object({
+ path: schema.maybe(schema.string()),
+ password: schema.maybe(schema.string()),
+ }),
redirectHttpFromPort: schema.maybe(schema.number()),
supportedProtocols: schema.arrayOf(
schema.oneOf([schema.literal('TLSv1'), schema.literal('TLSv1.1'), schema.literal('TLSv1.2')]),
@@ -56,8 +66,16 @@ export const sslSchema = schema.object(
},
{
validate: ssl => {
- if (ssl.enabled && (!ssl.key || !ssl.certificate)) {
- return 'must specify [certificate] and [key] when ssl is enabled';
+ if (ssl.key && ssl.keystore.path) {
+ return 'cannot use [key] when [keystore.path] is specified';
+ }
+
+ if (ssl.certificate && ssl.keystore.path) {
+ return 'cannot use [certificate] when [keystore.path] is specified';
+ }
+
+ if (ssl.enabled && (!ssl.key || !ssl.certificate) && !ssl.keystore.path) {
+ return 'must specify [certificate] and [key] -- or [keystore.path] -- when ssl is enabled';
}
if (!ssl.enabled && ssl.clientAuthentication !== 'none') {
@@ -88,14 +106,49 @@ export class SslConfig {
constructor(config: SslConfigType) {
this.enabled = config.enabled;
this.redirectHttpFromPort = config.redirectHttpFromPort;
- this.key = config.key;
- this.certificate = config.certificate;
- this.certificateAuthorities = this.initCertificateAuthorities(config.certificateAuthorities);
- this.keyPassphrase = config.keyPassphrase;
this.cipherSuites = config.cipherSuites;
this.supportedProtocols = config.supportedProtocols;
this.requestCert = config.clientAuthentication !== 'none';
this.rejectUnauthorized = config.clientAuthentication === 'required';
+
+ const addCAs = (ca: string[] | undefined) => {
+ if (ca && ca.length) {
+ this.certificateAuthorities = [...(this.certificateAuthorities || []), ...ca];
+ }
+ };
+
+ if (config.keystore?.path) {
+ const { key, cert, ca } = readPkcs12Keystore(config.keystore.path, config.keystore.password);
+ if (!key) {
+ throw new Error(`Did not find private key in keystore at [keystore.path].`);
+ } else if (!cert) {
+ throw new Error(`Did not find certificate in keystore at [keystore.path].`);
+ }
+ this.key = key;
+ this.certificate = cert;
+ addCAs(ca);
+ } else if (config.key && config.certificate) {
+ this.key = readFile(config.key);
+ this.keyPassphrase = config.keyPassphrase;
+ this.certificate = readFile(config.certificate);
+ }
+
+ if (config.truststore?.path) {
+ const ca = readPkcs12Truststore(config.truststore.path, config.truststore.password);
+ addCAs(ca);
+ }
+
+ const ca = config.certificateAuthorities;
+ if (ca) {
+ const parsed: string[] = [];
+ const paths = Array.isArray(ca) ? ca : [ca];
+ if (paths.length > 0) {
+ for (const path of paths) {
+ parsed.push(readFile(path));
+ }
+ addCAs(parsed);
+ }
+ }
}
/**
@@ -117,12 +170,8 @@ export class SslConfig {
: secureOptions | secureOption; // eslint-disable-line no-bitwise
}, 0);
}
-
- private initCertificateAuthorities(certificateAuthorities?: string[] | string) {
- if (certificateAuthorities === undefined || Array.isArray(certificateAuthorities)) {
- return certificateAuthorities;
- }
-
- return [certificateAuthorities];
- }
}
+
+const readFile = (file: string) => {
+ return readFileSync(file, 'utf8');
+};
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 953fa0738597c..eccf3985fc495 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -74,6 +74,7 @@ export { CspConfig, ICspConfig } from './csp';
export {
ClusterClient,
IClusterClient,
+ ICustomClusterClient,
Headers,
ScopedClusterClient,
IScopedClusterClient,
@@ -83,6 +84,7 @@ export {
ElasticsearchServiceSetup,
APICaller,
FakeRequest,
+ ScopeableRequest,
} from './elasticsearch';
export * from './elasticsearch/api_types';
export {
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index e17de7364ce59..cc36b90ec526d 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -249,8 +249,8 @@ export class LegacyService implements CoreService {
capabilities: setupDeps.core.capabilities,
context: setupDeps.core.context,
elasticsearch: {
- adminClient$: setupDeps.core.elasticsearch.adminClient$,
- dataClient$: setupDeps.core.elasticsearch.dataClient$,
+ adminClient: setupDeps.core.elasticsearch.adminClient,
+ dataClient: setupDeps.core.elasticsearch.dataClient,
createClient: setupDeps.core.elasticsearch.createClient,
},
http: {
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index 53849b040c413..073d380d3aa67 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -107,7 +107,7 @@ function createCoreSetupMock() {
const mock: MockedKeys = {
capabilities: capabilitiesServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
- elasticsearch: elasticsearchServiceMock.createSetupContract(),
+ elasticsearch: elasticsearchServiceMock.createSetup(),
http: httpMock,
savedObjects: savedObjectsServiceMock.createSetupContract(),
uiSettings: uiSettingsMock,
@@ -131,7 +131,7 @@ function createInternalCoreSetupMock() {
const setupDeps: InternalCoreSetup = {
capabilities: capabilitiesServiceMock.createSetupContract(),
context: contextServiceMock.createSetupContract(),
- elasticsearch: elasticsearchServiceMock.createSetupContract(),
+ elasticsearch: elasticsearchServiceMock.createInternalSetup(),
http: httpServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
savedObjects: savedObjectsServiceMock.createSetupContract(),
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 6e9a7967e9eca..6d82a8d3ec6cf 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -145,8 +145,8 @@ export function createPluginSetupContext(
createContextContainer: deps.context.createContextContainer,
},
elasticsearch: {
- adminClient$: deps.elasticsearch.adminClient$,
- dataClient$: deps.elasticsearch.dataClient$,
+ adminClient: deps.elasticsearch.adminClient,
+ dataClient: deps.elasticsearch.dataClient,
createClient: deps.elasticsearch.createClient,
},
http: {
diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts
index 18c04af3bb641..22dfbeecbaedd 100644
--- a/src/core/server/plugins/plugins_system.test.ts
+++ b/src/core/server/plugins/plugins_system.test.ts
@@ -414,3 +414,51 @@ test('`startPlugins` only starts plugins that were setup', async () => {
]
`);
});
+
+describe('setup', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+ it('throws timeout error if "setup" was not completed in 30 sec.', async () => {
+ const plugin: PluginWrapper = createPlugin('timeout-setup');
+ jest.spyOn(plugin, 'setup').mockImplementation(() => new Promise(i => i));
+ pluginsSystem.addPlugin(plugin);
+ mockCreatePluginSetupContext.mockImplementation(() => ({}));
+
+ const promise = pluginsSystem.setupPlugins(setupDeps);
+ jest.runAllTimers();
+
+ await expect(promise).rejects.toMatchInlineSnapshot(
+ `[Error: Setup lifecycle of "timeout-setup" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]`
+ );
+ });
+});
+
+describe('start', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+ it('throws timeout error if "start" was not completed in 30 sec.', async () => {
+ const plugin: PluginWrapper = createPlugin('timeout-start');
+ jest.spyOn(plugin, 'setup').mockResolvedValue({});
+ jest.spyOn(plugin, 'start').mockImplementation(() => new Promise(i => i));
+
+ pluginsSystem.addPlugin(plugin);
+ mockCreatePluginSetupContext.mockImplementation(() => ({}));
+ mockCreatePluginStartContext.mockImplementation(() => ({}));
+
+ await pluginsSystem.setupPlugins(setupDeps);
+ const promise = pluginsSystem.startPlugins(startDeps);
+ jest.runAllTimers();
+
+ await expect(promise).rejects.toMatchInlineSnapshot(
+ `[Error: Start lifecycle of "timeout-start" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.]`
+ );
+ });
+});
diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts
index f437b51e5b07a..dd2df7c8e01d1 100644
--- a/src/core/server/plugins/plugins_system.ts
+++ b/src/core/server/plugins/plugins_system.ts
@@ -23,7 +23,9 @@ import { PluginWrapper } from './plugin';
import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types';
import { createPluginSetupContext, createPluginStartContext } from './plugin_context';
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
+import { withTimeout } from '../../utils';
+const Sec = 1000;
/** @internal */
export class PluginsSystem {
private readonly plugins = new Map();
@@ -85,14 +87,16 @@ export class PluginsSystem {
return depContracts;
}, {} as Record);
- contracts.set(
- pluginName,
- await plugin.setup(
+ const contract = await withTimeout({
+ promise: plugin.setup(
createPluginSetupContext(this.coreContext, deps, plugin),
pluginDepContracts
- )
- );
+ ),
+ timeout: 30 * Sec,
+ errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`,
+ });
+ contracts.set(pluginName, contract);
this.satupPlugins.push(pluginName);
}
@@ -121,13 +125,16 @@ export class PluginsSystem {
return depContracts;
}, {} as Record);
- contracts.set(
- pluginName,
- await plugin.start(
+ const contract = await withTimeout({
+ promise: plugin.start(
createPluginStartContext(this.coreContext, deps, plugin),
pluginDepContracts
- )
- );
+ ),
+ timeout: 30 * Sec,
+ errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 30sec. Consider disabling the plugin and re-start.`,
+ });
+
+ contracts.set(pluginName, contract);
}
return contracts;
diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts
index f58939c58e85e..deb6f98e17b7b 100644
--- a/src/core/server/saved_objects/saved_objects_service.test.ts
+++ b/src/core/server/saved_objects/saved_objects_service.test.ts
@@ -23,7 +23,6 @@ import { SavedObjectsService, SavedObjectsSetupDeps } from './saved_objects_serv
import { mockCoreContext } from '../core_context.mock';
// @ts-ignore Typescript doesn't know about the jest mock
import { KibanaMigrator, mockKibanaMigratorInstance } from './migrations/kibana/kibana_migrator';
-import { of } from 'rxjs';
import * as legacyElasticsearch from 'elasticsearch';
import { Env } from '../config';
import { configServiceMock } from '../mocks';
@@ -49,7 +48,7 @@ describe('SavedObjectsService', () => {
const soService = new SavedObjectsService(coreContext);
const coreSetup = ({
- elasticsearch: { adminClient$: of(clusterClient) },
+ elasticsearch: { adminClient: clusterClient },
legacyPlugins: { uiExports: { savedObjectMappings: [] }, pluginExtendedConfig: {} },
} as unknown) as SavedObjectsSetupDeps;
@@ -68,7 +67,7 @@ describe('SavedObjectsService', () => {
});
const soService = new SavedObjectsService(coreContext);
const coreSetup = ({
- elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) },
+ elasticsearch: { adminClient: { callAsInternalUser: jest.fn() } },
legacyPlugins: { uiExports: {}, pluginExtendedConfig: {} },
} as unknown) as SavedObjectsSetupDeps;
@@ -82,7 +81,7 @@ describe('SavedObjectsService', () => {
const coreContext = mockCoreContext.create({ configService });
const soService = new SavedObjectsService(coreContext);
const coreSetup = ({
- elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) },
+ elasticsearch: { adminClient: { callAsInternalUser: jest.fn() } },
legacyPlugins: { uiExports: {}, pluginExtendedConfig: {} },
} as unknown) as SavedObjectsSetupDeps;
@@ -96,7 +95,7 @@ describe('SavedObjectsService', () => {
const coreContext = mockCoreContext.create({ configService });
const soService = new SavedObjectsService(coreContext);
const coreSetup = ({
- elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) },
+ elasticsearch: { adminClient: { callAsInternalUser: jest.fn() } },
legacyPlugins: { uiExports: {}, pluginExtendedConfig: {} },
} as unknown) as SavedObjectsSetupDeps;
diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts
index 5079b4f633df2..69ca8306ca4da 100644
--- a/src/core/server/saved_objects/saved_objects_service.ts
+++ b/src/core/server/saved_objects/saved_objects_service.ts
@@ -29,7 +29,7 @@ import {
import { KibanaMigrator, IKibanaMigrator } from './migrations';
import { CoreContext } from '../core_context';
import { LegacyServiceDiscoverPlugins } from '../legacy';
-import { ElasticsearchServiceSetup, APICaller } from '../elasticsearch';
+import { InternalElasticsearchServiceSetup, APICaller } from '../elasticsearch';
import { KibanaConfigType } from '../kibana_config';
import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster';
import { SavedObjectsConfigType } from './saved_objects_config';
@@ -174,7 +174,7 @@ export interface InternalSavedObjectsServiceStart extends SavedObjectsServiceSta
/** @internal */
export interface SavedObjectsSetupDeps {
legacyPlugins: LegacyServiceDiscoverPlugins;
- elasticsearch: ElasticsearchServiceSetup;
+ elasticsearch: InternalElasticsearchServiceSetup;
}
/** @internal */
@@ -206,8 +206,6 @@ export class SavedObjectsService
const savedObjectSchemas = new SavedObjectsSchema(savedObjectsSchemasDefinition);
- const adminClient = await setupDeps.elasticsearch.adminClient$.pipe(first()).toPromise();
-
const kibanaConfig = await this.coreContext.configService
.atPath('kibana')
.pipe(first())
@@ -218,6 +216,8 @@ export class SavedObjectsService
.pipe(first())
.toPromise();
+ const adminClient = setupDeps.elasticsearch.adminClient;
+
const migrator = (this.migrator = new KibanaMigrator({
savedObjectSchemas,
savedObjectMappings,
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index ef5bcecc948a9..70b8b2878c15b 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -141,7 +141,7 @@ export class SavedObjectsRepository {
callCluster: APICaller,
extraTypes: string[] = [],
injectedConstructor: any = SavedObjectsRepository
- ) {
+ ): ISavedObjectsRepository {
const mappings = migrator.getActiveMappings();
const allTypes = Object.keys(getRootPropertiesObjects(mappings));
const serializer = new SavedObjectsSerializer(schema);
diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts
index 0b67727455333..42f75e2517126 100644
--- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts
+++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts
@@ -66,12 +66,6 @@ export type ISavedObjectsClientProvider = Pick<
* Provider for the Scoped Saved Objects Client.
*
* @internal
- *
- * @internalRemarks Because `getClient` is synchronous the Client Provider does
- * not support creating factories that react to new ES clients emitted from
- * elasticsearch.adminClient$. The Client Provider therefore doesn't support
- * configuration changes to the Elasticsearch client. TODO: revisit once we've
- * closed https://github.com/elastic/kibana/pull/45796
*/
export class SavedObjectsClientProvider {
private readonly _wrapperFactories = new PriorityCollection<{
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 35359c5d6c417..c4f3bf6caf5bd 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -498,7 +498,7 @@ export type CapabilitiesSwitcher = (request: KibanaRequest, uiCapabilities: Capa
// @public
export class ClusterClient implements IClusterClient {
constructor(config: ElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders);
- asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): IScopedClusterClient;
+ asScoped(request?: ScopeableRequest): IScopedClusterClient;
callAsInternalUser: APICaller;
close(): void;
}
@@ -669,9 +669,9 @@ export class ElasticsearchErrorHelpers {
// @public (undocumented)
export interface ElasticsearchServiceSetup {
- readonly adminClient$: Observable;
- readonly createClient: (type: string, clientConfig?: Partial) => IClusterClient;
- readonly dataClient$: Observable;
+ readonly adminClient: IClusterClient;
+ readonly createClient: (type: string, clientConfig?: Partial) => ICustomClusterClient;
+ readonly dataClient: IClusterClient;
}
// @public (undocumented)
@@ -752,7 +752,7 @@ export interface HttpServiceStart {
export type IBasePath = Pick;
// @public
-export type IClusterClient = Pick;
+export type IClusterClient = Pick;
// @public
export interface IContextContainer> {
@@ -771,6 +771,9 @@ export interface ICspConfig {
readonly warnLegacyBrowsers: boolean;
}
+// @public
+export type ICustomClusterClient = Pick;
+
// @public
export interface IKibanaResponse {
// (undocumented)
@@ -885,7 +888,7 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory;
// @public
export const kibanaResponseFactory: {
- custom: | Buffer | Stream | {
+ custom: | {
message: string | Error;
attributes?: Record | undefined;
} | undefined>(options: CustomHttpResponseOptions) => KibanaResponse;
@@ -896,9 +899,9 @@ export const kibanaResponseFactory: {
conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse;
internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse;
customError: (options: CustomHttpResponseOptions) => KibanaResponse;
- redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>;
- ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>;
- accepted: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>;
+ redirected: (options: RedirectResponseOptions) => KibanaResponse>;
+ ok: (options?: HttpResponseOptions) => KibanaResponse>;
+ accepted: (options?: HttpResponseOptions) => KibanaResponse>;
noContent: (options?: HttpResponseOptions) => KibanaResponse;
};
@@ -1793,7 +1796,7 @@ export class SavedObjectsRepository {
// Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts
//
// @internal
- static createRepository(migrator: KibanaMigrator, schema: SavedObjectsSchema, config: LegacyConfig, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): any;
+ static createRepository(migrator: KibanaMigrator, schema: SavedObjectsSchema, config: LegacyConfig, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository;
delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>;
deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise;
// (undocumented)
@@ -1878,6 +1881,9 @@ export interface SavedObjectsUpdateResponse => {
+ // it consumes elasticsearch observables to provide the same client throughout the context lifetime.
const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise();
const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise();
const savedObjectsClient = coreSetup.savedObjects.getScopedClient(req);
@@ -226,8 +227,6 @@ export class Server {
render: rendering.render.bind(rendering, req, uiSettingsClient),
},
savedObjects: {
- // Note: the client provider doesn't support new ES clients
- // emitted from adminClient$
client: savedObjectsClient,
},
elasticsearch: {
diff --git a/src/core/utils/crypto/__fixtures__/README.md b/src/core/utils/crypto/__fixtures__/README.md
new file mode 100644
index 0000000000000..2652b16c420f0
--- /dev/null
+++ b/src/core/utils/crypto/__fixtures__/README.md
@@ -0,0 +1,44 @@
+# PKCS12 Test Fixtures
+
+These PKCS12 files are used to test different scenarios. Each has an empty password.
+
+Including `-noiter` uses a single encryption iteration, and `-nomaciter` uses a single MAC verification iteration.
+This makes each P12 keystore much quicker to parse.
+
+Commands to generate files:
+
+```shell
+# Generate a PKCS12 file with an EE cert and CA cert, but no EE key
+cat elasticsearch.crt ca.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -nokeys -out no_key.p12
+
+# Generate a PKCS12 file with an EE key and EE cert, but no CA cert
+cat elasticsearch.key elasticsearch.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -out no_ca.p12
+
+# Generate a PKCS12 file with an EE key, EE cert, and two CA certs
+cat elasticsearch.key elasticsearch.crt ca.crt ca.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -out two_cas.p12
+
+# Generate a PKCS12 file with two EE keys and EE certs
+cat elasticsearch.key elasticsearch.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -out two_keys.p12
+cat kibana.key kibana.crt | openssl pkcs12 -export -noiter -nomaciter -passout pass: -name 2 -out tmp.p12
+keytool -importkeystore -srckeystore tmp.p12 -srcstorepass '' -destkeystore two_keys.p12 -deststorepass '' -deststoretype PKCS12
+rm tmp.p12
+```
+
+No commonly available tools seem to be able to generate a PKCS12 file with a key and no certificate, so we use node-forge to do that:
+
+```js
+const utils = require('@kbn/dev-utils');
+const forge = require('node-forge');
+const fs = require('fs');
+
+const pemCA = fs.readFileSync(utils.CA_CERT_PATH, 'utf8');
+const pemKey = fs.readFileSync(utils.ES_KEY_PATH, 'utf8');
+const privateKey = forge.pki.privateKeyFromPem(pemKey);
+
+const p12Asn = forge.pkcs12.toPkcs12Asn1(privateKey, pemCA, null, {
+ useMac: false,
+ generateLocalKeyId: false,
+});
+const p12Der = forge.asn1.toDer(p12Asn).getBytes();
+fs.writeFileSync('no_cert.p12', p12Der, { encoding: 'binary' });
+```
diff --git a/src/core/utils/crypto/__fixtures__/index.ts b/src/core/utils/crypto/__fixtures__/index.ts
new file mode 100644
index 0000000000000..12481963ff4f6
--- /dev/null
+++ b/src/core/utils/crypto/__fixtures__/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { resolve } from 'path';
+
+export const NO_CA_PATH = resolve(__dirname, './no_ca.p12');
+export const NO_CERT_PATH = resolve(__dirname, './no_cert.p12');
+export const NO_KEY_PATH = resolve(__dirname, './no_key.p12');
+export const TWO_CAS_PATH = resolve(__dirname, './two_cas.p12');
+export const TWO_KEYS_PATH = resolve(__dirname, './two_keys.p12');
diff --git a/src/core/utils/crypto/__fixtures__/no_ca.p12 b/src/core/utils/crypto/__fixtures__/no_ca.p12
new file mode 100644
index 0000000000000..1e6df9a0f71c5
Binary files /dev/null and b/src/core/utils/crypto/__fixtures__/no_ca.p12 differ
diff --git a/src/core/utils/crypto/__fixtures__/no_cert.p12 b/src/core/utils/crypto/__fixtures__/no_cert.p12
new file mode 100644
index 0000000000000..8453fe878e785
Binary files /dev/null and b/src/core/utils/crypto/__fixtures__/no_cert.p12 differ
diff --git a/src/core/utils/crypto/__fixtures__/no_key.p12 b/src/core/utils/crypto/__fixtures__/no_key.p12
new file mode 100644
index 0000000000000..1bffb7a301b2e
Binary files /dev/null and b/src/core/utils/crypto/__fixtures__/no_key.p12 differ
diff --git a/src/core/utils/crypto/__fixtures__/two_cas.p12 b/src/core/utils/crypto/__fixtures__/two_cas.p12
new file mode 100644
index 0000000000000..25784a6fb9a94
Binary files /dev/null and b/src/core/utils/crypto/__fixtures__/two_cas.p12 differ
diff --git a/src/core/utils/crypto/__fixtures__/two_keys.p12 b/src/core/utils/crypto/__fixtures__/two_keys.p12
new file mode 100644
index 0000000000000..c934b34901a93
Binary files /dev/null and b/src/core/utils/crypto/__fixtures__/two_keys.p12 differ
diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts b/src/core/utils/crypto/index.ts
similarity index 88%
rename from src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts
rename to src/core/utils/crypto/index.ts
index f6c6079822cb5..9a36682cc4ecb 100644
--- a/src/core/server/elasticsearch/elasticsearch_client_config.test.mocks.ts
+++ b/src/core/utils/crypto/index.ts
@@ -17,5 +17,4 @@
* under the License.
*/
-export const mockReadFileSync = jest.fn();
-jest.mock('fs', () => ({ readFileSync: mockReadFileSync }));
+export { Pkcs12ReadResult, readPkcs12Keystore, readPkcs12Truststore } from './pkcs12';
diff --git a/src/core/utils/crypto/pkcs12.test.ts b/src/core/utils/crypto/pkcs12.test.ts
new file mode 100644
index 0000000000000..c6c28697c4bcc
--- /dev/null
+++ b/src/core/utils/crypto/pkcs12.test.ts
@@ -0,0 +1,205 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ CA_CERT_PATH,
+ ES_KEY_PATH,
+ ES_CERT_PATH,
+ ES_P12_PATH,
+ ES_P12_PASSWORD,
+ ES_EMPTYPASSWORD_P12_PATH,
+ ES_NOPASSWORD_P12_PATH,
+} from '@kbn/dev-utils';
+import { NO_CA_PATH, NO_CERT_PATH, NO_KEY_PATH, TWO_CAS_PATH, TWO_KEYS_PATH } from './__fixtures__';
+import { readFileSync } from 'fs';
+
+import { readPkcs12Keystore, Pkcs12ReadResult, readPkcs12Truststore } from '.';
+
+const reformatPem = (pem: string) => {
+ // ensure consistency in line endings when comparing two PEM files
+ return pem.replace(/\r\n/g, '\n').trim();
+};
+
+const readPem = (file: string) => {
+ const raw = readFileSync(file, 'utf8');
+ // strip bag attributes that are included from a previous PKCS #12 export
+ const pem = raw.substr(raw.indexOf('-----BEGIN'));
+ return reformatPem(pem);
+};
+
+let pemCA: string;
+let pemCert: string;
+let pemKey: string;
+
+beforeAll(() => {
+ pemCA = readPem(CA_CERT_PATH);
+ pemCert = readPem(ES_CERT_PATH);
+ pemKey = readPem(ES_KEY_PATH);
+});
+
+describe('#readPkcs12Keystore', () => {
+ const expectKey = (pkcs12ReadResult: Pkcs12ReadResult) => {
+ const result = reformatPem(pkcs12ReadResult.key!);
+ expect(result).toEqual(pemKey);
+ };
+
+ const expectCert = (pkcs12ReadResult: Pkcs12ReadResult) => {
+ const result = reformatPem(pkcs12ReadResult.cert!);
+ expect(result).toEqual(pemCert);
+ };
+
+ const expectCA = (pkcs12ReadResult: Pkcs12ReadResult, ca = [pemCA]) => {
+ const result = pkcs12ReadResult.ca?.map(x => reformatPem(x));
+ expect(result).toEqual(ca);
+ };
+
+ describe('Succeeds when the correct password is used', () => {
+ let pkcs12ReadResult: Pkcs12ReadResult;
+
+ beforeAll(() => {
+ // this is expensive, just do it once
+ pkcs12ReadResult = readPkcs12Keystore(ES_P12_PATH, ES_P12_PASSWORD);
+ });
+
+ it('Extracts the PEM key', () => {
+ expectKey(pkcs12ReadResult);
+ });
+
+ it('Extracts the PEM instance certificate', () => {
+ expectCert(pkcs12ReadResult);
+ });
+
+ it('Extracts the PEM CA certificate', () => {
+ expectCA(pkcs12ReadResult);
+ });
+ });
+
+ describe('Succeeds on a key store with an empty password', () => {
+ let pkcs12ReadResult: Pkcs12ReadResult;
+
+ beforeAll(() => {
+ // this is expensive, just do it once
+ pkcs12ReadResult = readPkcs12Keystore(ES_EMPTYPASSWORD_P12_PATH, '');
+ });
+
+ it('Extracts the PEM key', () => {
+ expectKey(pkcs12ReadResult);
+ });
+
+ it('Extracts the PEM instance certificate', () => {
+ expectCert(pkcs12ReadResult);
+ });
+
+ it('Extracts the PEM CA certificate', () => {
+ expectCA(pkcs12ReadResult);
+ });
+ });
+
+ describe('Succeeds on a key store with no password', () => {
+ let pkcs12ReadResult: Pkcs12ReadResult;
+
+ beforeAll(() => {
+ // this is expensive, just do it once
+ pkcs12ReadResult = readPkcs12Keystore(ES_NOPASSWORD_P12_PATH);
+ });
+
+ it('Extracts the PEM key', () => {
+ expectKey(pkcs12ReadResult);
+ });
+
+ it('Extracts the PEM instance certificate', () => {
+ expectCert(pkcs12ReadResult);
+ });
+
+ it('Extracts the PEM CA certificate', () => {
+ expectCA(pkcs12ReadResult);
+ });
+ });
+
+ describe('Handles other key store permutations', () => {
+ it('Succeeds with no key', () => {
+ const pkcs12ReadResult = readPkcs12Keystore(NO_KEY_PATH, '');
+ expectCA(pkcs12ReadResult, [pemCert, pemCA]);
+ expect(pkcs12ReadResult.cert).toBeUndefined();
+ expect(pkcs12ReadResult.key).toBeUndefined();
+ });
+
+ it('Succeeds with no instance certificate', () => {
+ const pkcs12ReadResult = readPkcs12Keystore(NO_CERT_PATH, '');
+ expectCA(pkcs12ReadResult);
+ expect(pkcs12ReadResult.cert).toBeUndefined();
+ expectKey(pkcs12ReadResult);
+ });
+
+ it('Succeeds with no CA certificate', () => {
+ const pkcs12ReadResult = readPkcs12Keystore(NO_CA_PATH, '');
+ expect(pkcs12ReadResult.ca).toBeUndefined();
+ expectCert(pkcs12ReadResult);
+ expectKey(pkcs12ReadResult);
+ });
+
+ it('Succeeds with two CA certificates', () => {
+ const pkcs12ReadResult = readPkcs12Keystore(TWO_CAS_PATH, '');
+ expectCA(pkcs12ReadResult, [pemCA, pemCA]);
+ expectCert(pkcs12ReadResult);
+ expectKey(pkcs12ReadResult);
+ });
+ });
+
+ describe('Throws errors', () => {
+ const expectError = (password?: string) => {
+ expect(() => readPkcs12Keystore(ES_P12_PATH, password)).toThrowError(
+ 'PKCS#12 MAC could not be verified. Invalid password?'
+ );
+ };
+
+ it('When an invalid password is used (incorrect)', () => {
+ expectError('incorrect');
+ });
+
+ it('When an invalid password is used (empty)', () => {
+ expectError('');
+ });
+
+ it('When an invalid password is used (undefined)', () => {
+ expectError();
+ });
+
+ it('When an invalid file path is used', () => {
+ const path = 'invalid-filepath';
+ expect(() => readPkcs12Keystore(path)).toThrowError(
+ `ENOENT: no such file or directory, open '${path}'`
+ );
+ });
+
+ it('When two keys are present', () => {
+ expect(() => readPkcs12Keystore(TWO_KEYS_PATH, '')).toThrowError(
+ 'Keystore contains multiple private keys.'
+ );
+ });
+ });
+});
+
+describe('#readPkcs12Truststore', () => {
+ it('reads all certificates into one CA array and discards any certificates that have keys', () => {
+ const ca = readPkcs12Truststore(ES_P12_PATH, ES_P12_PASSWORD);
+ const result = ca?.map(x => reformatPem(x));
+ expect(result).toEqual([pemCA]);
+ });
+});
diff --git a/src/core/utils/crypto/pkcs12.ts b/src/core/utils/crypto/pkcs12.ts
new file mode 100644
index 0000000000000..74f22bb685c2f
--- /dev/null
+++ b/src/core/utils/crypto/pkcs12.ts
@@ -0,0 +1,179 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { util, asn1, pkcs12, pki } from 'node-forge';
+import { readFileSync } from 'fs';
+
+export interface Pkcs12ReadResult {
+ ca?: string[];
+ cert?: string;
+ key?: string;
+}
+
+/**
+ * Reads a private key and certificate chain from a PKCS12 key store.
+ *
+ * @remarks
+ * The PKCS12 key store may contain the following:
+ * - 0 or more certificates contained in a `certBag` (OID
+ * 1.2.840.113549.1.12.10.1.3); if a certificate has an associated
+ * private key it is treated as an instance certificate, otherwise it is
+ * treated as a CA certificate
+ * - 0 or 1 private keys contained in a `keyBag` (OID
+ * 1.2.840.113549.1.12.10.1.1) or a `pkcs8ShroudedKeyBag` (OID
+ * 1.2.840.113549.1.12.10.1.2)
+ *
+ * Any other PKCS12 bags are ignored.
+ *
+ * @privateRemarks
+ * This intentionally does not allow for a separate key store password and
+ * private key password. In conventional implementations, these two values
+ * are expected to be identical, so we do not support other configurations.
+ *
+ * @param path The file path of the PKCS12 key store
+ * @param password The optional password of the key store and private key;
+ * if there is no password, this may be an empty string or `undefined`,
+ * depending on how the key store was generated.
+ * @returns the parsed private key and certificate(s) in PEM format
+ */
+export const readPkcs12Keystore = (path: string, password?: string): Pkcs12ReadResult => {
+ const p12base64 = readFileSync(path, 'base64');
+ const p12Der = util.decode64(p12base64);
+ const p12Asn1 = asn1.fromDer(p12Der);
+ const p12 = pkcs12.pkcs12FromAsn1(p12Asn1, password);
+ const keyObj = getKey(p12);
+ const { ca, cert } = getCerts(p12, keyObj?.publicKeyData);
+ return { ca, cert, key: keyObj?.key };
+};
+
+/**
+ * Reads a certificate chain from a PKCS12 trust store.
+ *
+ * @remarks
+ * The PKCS12 trust store may contain the following:
+ * - 0 or more certificates contained in a `certBag` (OID
+ * 1.2.840.113549.1.12.10.1.3); all are treated as CA certificates
+ *
+ * Any other PKCS12 bags are ignored.
+ *
+ * @param path The file path of the PKCS12 trust store
+ * @param password The optional password of the trust store; if there is
+ * no password, this may be an empty string or `undefined`, depending on
+ * how the trust store was generated.
+ * @returns the parsed certificate(s) in PEM format
+ */
+export const readPkcs12Truststore = (path: string, password?: string): string[] | undefined => {
+ const p12base64 = readFileSync(path, 'base64');
+ const p12Der = util.decode64(p12base64);
+ const p12Asn1 = asn1.fromDer(p12Der);
+ const p12 = pkcs12.pkcs12FromAsn1(p12Asn1, password);
+ const keyObj = getKey(p12);
+ const { ca } = getCerts(p12, keyObj?.publicKeyData);
+ return ca;
+};
+
+// jsbn.BigInteger as described in type definition is wrong, it doesn't include `compareTo`
+interface BigInteger {
+ data: number[];
+ t: number;
+ s: number;
+ toString(): string;
+ compareTo(bn: BigInteger): number;
+}
+
+interface PublicKeyData {
+ n: BigInteger; // modulus
+ e: BigInteger; // public exponent
+}
+
+const doesPubKeyMatch = (a?: PublicKeyData, b?: PublicKeyData) => {
+ if (a && b) {
+ return a.n.compareTo(b.n) === 0 && a.e.compareTo(b.e) === 0;
+ }
+ return false;
+};
+
+const getCerts = (p12: pkcs12.Pkcs12Pfx, pubKey?: PublicKeyData) => {
+ // OID 1.2.840.113549.1.12.10.1.3 (certBag)
+ const bags = getBags(p12, pki.oids.certBag);
+ let ca;
+ let cert;
+ if (bags && bags.length) {
+ const certs = bags.map(convertCert).filter(x => x !== undefined);
+ cert = certs.find(x => doesPubKeyMatch(x!.publicKeyData, pubKey))?.cert;
+ ca = certs.filter(x => !doesPubKeyMatch(x!.publicKeyData, pubKey)).map(x => x!.cert);
+ if (ca.length === 0) {
+ ca = undefined;
+ }
+ }
+
+ return { ca, cert };
+};
+
+export const convertCert = (bag: pkcs12.Bag) => {
+ const cert = bag.cert;
+ if (cert) {
+ const pem = pki.certificateToPem(cert);
+ const key = cert.publicKey as pki.rsa.PublicKey;
+ const publicKeyData: PublicKeyData = {
+ n: key.n as BigInteger,
+ e: key.e as BigInteger,
+ };
+ return {
+ cert: pem,
+ publicKeyData,
+ };
+ }
+ return undefined;
+};
+
+const getKey = (p12: pkcs12.Pkcs12Pfx) => {
+ // OID 1.2.840.113549.1.12.10.1.1 (keyBag) || OID 1.2.840.113549.1.12.10.1.2 (pkcs8ShroudedKeyBag)
+ const bags = [
+ ...(getBags(p12, pki.oids.keyBag) || []),
+ ...(getBags(p12, pki.oids.pkcs8ShroudedKeyBag) || []),
+ ];
+ if (bags && bags.length) {
+ if (bags.length > 1) {
+ throw new Error(`Keystore contains multiple private keys.`);
+ }
+ const key = bags[0].key as pki.rsa.PrivateKey;
+ if (key) {
+ const pem = pki.privateKeyToPem(key);
+ const publicKeyData: PublicKeyData = {
+ n: key.n as BigInteger,
+ e: key.e as BigInteger,
+ };
+ return {
+ key: pem,
+ publicKeyData,
+ };
+ }
+ }
+ return undefined;
+};
+
+const getBags = (p12: pkcs12.Pkcs12Pfx, bagType: string) => {
+ const bagObj = p12.getBags({ bagType });
+ const bags = bagObj[bagType];
+ if (bags && bags.length) {
+ return bags;
+ }
+ return undefined;
+};
diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts
index b51cc4ef56410..7c8ed481c0a7d 100644
--- a/src/core/utils/index.ts
+++ b/src/core/utils/index.ts
@@ -19,10 +19,12 @@
export * from './assert_never';
export * from './context';
+export * from './crypto';
export * from './deep_freeze';
export * from './get';
export * from './map_to_object';
export * from './merge';
export * from './pick';
+export * from './promise';
export * from './url';
export * from './unset';
diff --git a/src/core/utils/promise.test.ts b/src/core/utils/promise.test.ts
new file mode 100644
index 0000000000000..97d16c5158c7b
--- /dev/null
+++ b/src/core/utils/promise.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { withTimeout } from './promise';
+
+const delay = (ms: number, resolveValue?: any) =>
+ new Promise(resolve => setTimeout(resolve, ms, resolveValue));
+
+describe('withTimeout', () => {
+ it('resolves with a promise value if resolved in given timeout', async () => {
+ await expect(
+ withTimeout({
+ promise: delay(10, 'value'),
+ timeout: 200,
+ errorMessage: 'error-message',
+ })
+ ).resolves.toBe('value');
+ });
+
+ it('rejects with errorMessage if not resolved in given time', async () => {
+ await expect(
+ withTimeout({
+ promise: delay(200, 'value'),
+ timeout: 10,
+ errorMessage: 'error-message',
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: error-message]`);
+
+ await expect(
+ withTimeout({
+ promise: new Promise(i => i),
+ timeout: 10,
+ errorMessage: 'error-message',
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: error-message]`);
+ });
+
+ it('does not swallow promise error', async () => {
+ await expect(
+ withTimeout({
+ promise: Promise.reject(new Error('from-promise')),
+ timeout: 10,
+ errorMessage: 'error-message',
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`);
+ });
+});
diff --git a/src/core/utils/promise.ts b/src/core/utils/promise.ts
new file mode 100644
index 0000000000000..ae1db9c54eda5
--- /dev/null
+++ b/src/core/utils/promise.ts
@@ -0,0 +1,33 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export function withTimeout({
+ promise,
+ timeout,
+ errorMessage,
+}: {
+ promise: Promise;
+ timeout: number;
+ errorMessage: string;
+}) {
+ return Promise.race([
+ promise,
+ new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)),
+ ]) as Promise;
+}
diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js
index 27335b35374ba..19d74bcf89e30 100644
--- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js
+++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js
@@ -48,6 +48,8 @@ export const CleanClientModulesOnDLLTask = {
];
const discoveredLegacyCorePluginEntries = await globby([
`${baseDir}/src/legacy/core_plugins/*/index.js`,
+ // Small exception to load dynamically discovered functions for timelion plugin
+ `${baseDir}/src/legacy/core_plugins/timelion/server/*_functions/**/*.js`,
`!${baseDir}/src/legacy/core_plugins/**/public`,
]);
const discoveredPluginEntries = await globby([
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
index d1734e836d983..31a5a4c13a4be 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker
@@ -34,6 +34,10 @@ kibana_vars=(
elasticsearch.ssl.certificateAuthorities
elasticsearch.ssl.key
elasticsearch.ssl.keyPassphrase
+ elasticsearch.ssl.keystore.path
+ elasticsearch.ssl.keystore.password
+ elasticsearch.ssl.truststore.path
+ elasticsearch.ssl.truststore.password
elasticsearch.ssl.verificationMode
elasticsearch.startupTimeout
elasticsearch.username
@@ -73,6 +77,10 @@ kibana_vars=(
server.ssl.enabled
server.ssl.key
server.ssl.keyPassphrase
+ server.ssl.keystore.path
+ server.ssl.keystore.password
+ server.ssl.truststore.path
+ server.ssl.truststore.password
server.ssl.redirectHttpFromPort
server.ssl.supportedProtocols
server.xsrf.whitelist
diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js
index 9321336f0f55e..1c95e75396bcc 100644
--- a/src/dev/jest/config.js
+++ b/src/dev/jest/config.js
@@ -69,7 +69,7 @@ export default {
],
setupFilesAfterEnv: ['/src/dev/jest/setup/mocks.js'],
coverageDirectory: '/target/kibana-coverage/jest',
- coverageReporters: ['html', 'text'],
+ coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'],
moduleFileExtensions: ['js', 'json', 'ts', 'tsx'],
modulePathIgnorePatterns: ['__fixtures__/', 'target/'],
testMatch: ['**/*.test.{js,ts,tsx}'],
diff --git a/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js b/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js
index 2942e76f27628..ec7d256975b27 100644
--- a/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js
+++ b/src/legacy/core_plugins/console/server/__tests__/elasticsearch_proxy_config.js
@@ -19,14 +19,10 @@
import expect from '@kbn/expect';
import moment from 'moment';
-import fs from 'fs';
-import { promisify } from 'bluebird';
import { getElasticsearchProxyConfig } from '../elasticsearch_proxy_config';
import https from 'https';
import http from 'http';
-const readFileAsync = promisify(fs.readFile, fs);
-
const getDefaultElasticsearchConfig = () => {
return {
hosts: ['http://localhost:9200', 'http://192.168.1.1:1234'],
@@ -124,10 +120,10 @@ describe('plugins/console', function() {
it(`sets ca when certificateAuthorities are specified`, function() {
const { agent } = getElasticsearchProxyConfig({
...config,
- ssl: { ...config.ssl, certificateAuthorities: [__dirname + '/fixtures/ca.crt'] },
+ ssl: { ...config.ssl, certificateAuthorities: ['content-of-some-path'] },
});
- expect(agent.options.ca).to.contain('test ca certificate\n');
+ expect(agent.options.ca).to.contain('content-of-some-path');
});
describe('when alwaysPresentCertificate is false', () => {
@@ -137,8 +133,8 @@ describe('plugins/console', function() {
ssl: {
...config.ssl,
alwaysPresentCertificate: false,
- certificate: __dirname + '/fixtures/cert.crt',
- key: __dirname + '/fixtures/cert.key',
+ certificate: 'content-of-some-path',
+ key: 'content-of-another-path',
},
});
@@ -152,8 +148,8 @@ describe('plugins/console', function() {
ssl: {
...config.ssl,
alwaysPresentCertificate: false,
- certificate: __dirname + '/fixtures/cert.crt',
- key: __dirname + '/fixtures/cert.key',
+ certificate: 'content-of-some-path',
+ key: 'content-of-another-path',
keyPassphrase: 'secret',
},
});
@@ -163,22 +159,19 @@ describe('plugins/console', function() {
});
describe('when alwaysPresentCertificate is true', () => {
- it(`sets cert and key when certificate and key paths are specified`, async function() {
- const certificatePath = __dirname + '/fixtures/cert.crt';
- const keyPath = __dirname + '/fixtures/cert.key';
-
+ it(`sets cert and key when certificate and key are specified`, async function() {
const { agent } = getElasticsearchProxyConfig({
...config,
ssl: {
...config.ssl,
alwaysPresentCertificate: true,
- certificate: certificatePath,
- key: keyPath,
+ certificate: 'content-of-some-path',
+ key: 'content-of-another-path',
},
});
- expect(agent.options.cert).to.be(await readFileAsync(certificatePath, 'utf8'));
- expect(agent.options.key).to.be(await readFileAsync(keyPath, 'utf8'));
+ expect(agent.options.cert).to.be('content-of-some-path');
+ expect(agent.options.key).to.be('content-of-another-path');
});
it(`sets passphrase when certificate, key and keyPassphrase are specified`, function() {
@@ -187,8 +180,8 @@ describe('plugins/console', function() {
ssl: {
...config.ssl,
alwaysPresentCertificate: true,
- certificate: __dirname + '/fixtures/cert.crt',
- key: __dirname + '/fixtures/cert.key',
+ certificate: 'content-of-some-path',
+ key: 'content-of-another-path',
keyPassphrase: 'secret',
},
});
@@ -197,13 +190,12 @@ describe('plugins/console', function() {
});
it(`doesn't set cert when only certificate path is specified`, async function() {
- const certificatePath = __dirname + '/fixtures/cert.crt';
const { agent } = getElasticsearchProxyConfig({
...config,
ssl: {
...config.ssl,
alwaysPresentCertificate: true,
- certificate: certificatePath,
+ certificate: 'content-of-some-path',
key: undefined,
},
});
@@ -213,14 +205,13 @@ describe('plugins/console', function() {
});
it(`doesn't set key when only key path is specified`, async function() {
- const keyPath = __dirname + '/fixtures/cert.key';
const { agent } = getElasticsearchProxyConfig({
...config,
ssl: {
...config.ssl,
alwaysPresentCertificate: true,
certificate: undefined,
- key: keyPath,
+ key: 'content-of-some-path',
},
});
diff --git a/src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt b/src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt
deleted file mode 100644
index 075fdd038daff..0000000000000
--- a/src/legacy/core_plugins/console/server/__tests__/fixtures/ca.crt
+++ /dev/null
@@ -1 +0,0 @@
-test ca certificate
diff --git a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt b/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt
deleted file mode 100644
index 360cdfaaaa5a9..0000000000000
--- a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.crt
+++ /dev/null
@@ -1 +0,0 @@
-test certificate
diff --git a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key b/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key
deleted file mode 100644
index 04d3bfef24188..0000000000000
--- a/src/legacy/core_plugins/console/server/__tests__/fixtures/cert.key
+++ /dev/null
@@ -1 +0,0 @@
-test key
diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_config.js b/src/legacy/core_plugins/console/server/__tests__/proxy_config.js
index 821fb8bef64d5..2a221aa261167 100644
--- a/src/legacy/core_plugins/console/server/__tests__/proxy_config.js
+++ b/src/legacy/core_plugins/console/server/__tests__/proxy_config.js
@@ -21,7 +21,6 @@
import expect from '@kbn/expect';
import sinon from 'sinon';
-import fs from 'fs';
import https, { Agent as HttpsAgent } from 'https';
import { parse as parseUrl } from 'url';
@@ -36,14 +35,6 @@ const parsedGoogle = parseUrl('https://google.com/search');
const parsedLocalEs = parseUrl('https://localhost:5601/search');
describe('ProxyConfig', function() {
- beforeEach(function() {
- sinon.stub(fs, 'readFileSync').callsFake(path => ({ path }));
- });
-
- afterEach(function() {
- fs.readFileSync.restore();
- });
-
describe('constructor', function() {
beforeEach(function() {
sinon.stub(https, 'Agent');
@@ -56,7 +47,7 @@ describe('ProxyConfig', function() {
it('uses ca to create sslAgent', function() {
const config = new ProxyConfig({
ssl: {
- ca: ['path/to/ca'],
+ ca: ['content-of-some-path'],
},
});
@@ -64,7 +55,7 @@ describe('ProxyConfig', function() {
sinon.assert.calledOnce(https.Agent);
const sslAgentOpts = https.Agent.firstCall.args[0];
expect(sslAgentOpts).to.eql({
- ca: [{ path: 'path/to/ca' }],
+ ca: ['content-of-some-path'],
cert: undefined,
key: undefined,
rejectUnauthorized: true,
@@ -74,8 +65,8 @@ describe('ProxyConfig', function() {
it('uses cert, and key to create sslAgent', function() {
const config = new ProxyConfig({
ssl: {
- cert: 'path/to/cert',
- key: 'path/to/key',
+ cert: 'content-of-some-path',
+ key: 'content-of-another-path',
},
});
@@ -84,8 +75,8 @@ describe('ProxyConfig', function() {
const sslAgentOpts = https.Agent.firstCall.args[0];
expect(sslAgentOpts).to.eql({
ca: undefined,
- cert: { path: 'path/to/cert' },
- key: { path: 'path/to/key' },
+ cert: 'content-of-some-path',
+ key: 'content-of-another-path',
rejectUnauthorized: true,
});
});
@@ -93,9 +84,9 @@ describe('ProxyConfig', function() {
it('uses ca, cert, and key to create sslAgent', function() {
const config = new ProxyConfig({
ssl: {
- ca: ['path/to/ca'],
- cert: 'path/to/cert',
- key: 'path/to/key',
+ ca: ['content-of-some-path'],
+ cert: 'content-of-another-path',
+ key: 'content-of-yet-another-path',
rejectUnauthorized: true,
},
});
@@ -104,9 +95,9 @@ describe('ProxyConfig', function() {
sinon.assert.calledOnce(https.Agent);
const sslAgentOpts = https.Agent.firstCall.args[0];
expect(sslAgentOpts).to.eql({
- ca: [{ path: 'path/to/ca' }],
- cert: { path: 'path/to/cert' },
- key: { path: 'path/to/key' },
+ ca: ['content-of-some-path'],
+ cert: 'content-of-another-path',
+ key: 'content-of-yet-another-path',
rejectUnauthorized: true,
});
});
diff --git a/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts b/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts
index 9e7c814aa57a4..5f44a524e9cc8 100644
--- a/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts
+++ b/src/legacy/core_plugins/console/server/elasticsearch_proxy_config.ts
@@ -18,13 +18,10 @@
*/
import _ from 'lodash';
-import { readFileSync } from 'fs';
import http from 'http';
import https from 'https';
import url from 'url';
-const readFile = (file: string) => readFileSync(file, 'utf8');
-
const createAgent = (legacyConfig: any) => {
const target = url.parse(_.head(legacyConfig.hosts));
if (!/^https/.test(target.protocol || '')) return new http.Agent();
@@ -49,22 +46,12 @@ const createAgent = (legacyConfig: any) => {
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
}
- if (
- legacyConfig.ssl &&
- Array.isArray(legacyConfig.ssl.certificateAuthorities) &&
- legacyConfig.ssl.certificateAuthorities.length > 0
- ) {
- agentOptions.ca = legacyConfig.ssl.certificateAuthorities.map(readFile);
- }
+ agentOptions.ca = legacyConfig.ssl?.certificateAuthorities;
- if (
- legacyConfig.ssl &&
- legacyConfig.ssl.alwaysPresentCertificate &&
- legacyConfig.ssl.certificate &&
- legacyConfig.ssl.key
- ) {
- agentOptions.cert = readFile(legacyConfig.ssl.certificate);
- agentOptions.key = readFile(legacyConfig.ssl.key);
+ const ignoreCertAndKey = !legacyConfig.ssl?.alwaysPresentCertificate;
+ if (!ignoreCertAndKey && legacyConfig.ssl?.certificate && legacyConfig.ssl?.key) {
+ agentOptions.cert = legacyConfig.ssl.certificate;
+ agentOptions.key = legacyConfig.ssl.key;
agentOptions.passphrase = legacyConfig.ssl.keyPassphrase;
}
diff --git a/src/legacy/core_plugins/console/server/proxy_config.js b/src/legacy/core_plugins/console/server/proxy_config.js
index 44a2769a46eb3..b8c1b11f9a0d3 100644
--- a/src/legacy/core_plugins/console/server/proxy_config.js
+++ b/src/legacy/core_plugins/console/server/proxy_config.js
@@ -20,7 +20,6 @@
import { values } from 'lodash';
import { format as formatUrl } from 'url';
import { Agent as HttpsAgent } from 'https';
-import { readFileSync } from 'fs';
import { WildcardMatcher } from './wildcard_matcher';
@@ -63,9 +62,9 @@ export class ProxyConfig {
this.verifySsl = ssl.verify;
const sslAgentOpts = {
- ca: ssl.ca && ssl.ca.map(ca => readFileSync(ca)),
- cert: ssl.cert && readFileSync(ssl.cert),
- key: ssl.key && readFileSync(ssl.key),
+ ca: ssl.ca,
+ cert: ssl.cert,
+ key: ssl.key,
};
if (values(sslAgentOpts).filter(Boolean).length) {
diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js
index 7fc00e27e5b5f..da7b557e7ea19 100644
--- a/src/legacy/core_plugins/elasticsearch/index.js
+++ b/src/legacy/core_plugins/elasticsearch/index.js
@@ -16,9 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-import { combineLatest } from 'rxjs';
-import { first, map } from 'rxjs/operators';
+import { first } from 'rxjs/operators';
import healthCheck from './lib/health_check';
import { Cluster } from './lib/cluster';
import { createProxy } from './lib/create_proxy';
@@ -36,19 +34,12 @@ export default function(kibana) {
// All methods that ES plugin exposes are synchronous so we should get the first
// value from all observables here to be able to synchronously return and create
// cluster clients afterwards.
- const [esConfig, adminCluster, dataCluster] = await combineLatest(
- server.newPlatform.__internals.elasticsearch.legacy.config$,
- server.newPlatform.setup.core.elasticsearch.adminClient$,
- server.newPlatform.setup.core.elasticsearch.dataClient$
- )
- .pipe(
- first(),
- map(([config, adminClusterClient, dataClusterClient]) => [
- config,
- new Cluster(adminClusterClient),
- new Cluster(dataClusterClient),
- ])
- )
+ const { adminClient, dataClient } = server.newPlatform.setup.core.elasticsearch;
+ const adminCluster = new Cluster(adminClient);
+ const dataCluster = new Cluster(dataClient);
+
+ const esConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$
+ .pipe(first())
.toPromise();
defaultVars = {
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx
index c2f716ff6c45a..b83ac5b4a7795 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx
@@ -19,6 +19,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui';
import moment from 'moment-timezone';
+import { unitOfTime } from 'moment';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json';
@@ -53,6 +54,58 @@ interface DiscoverHistogramState {
chartsTheme: EuiChartThemeType['theme'];
}
+function findIntervalFromDuration(
+ dateValue: number,
+ esValue: number,
+ esUnit: unitOfTime.Base,
+ timeZone: string
+) {
+ const date = moment.tz(dateValue, timeZone);
+ const startOfDate = moment.tz(date, timeZone).startOf(esUnit);
+ const endOfDate = moment
+ .tz(date, timeZone)
+ .startOf(esUnit)
+ .add(esValue, esUnit);
+ return endOfDate.valueOf() - startOfDate.valueOf();
+}
+
+function getIntervalInMs(
+ value: number,
+ esValue: number,
+ esUnit: unitOfTime.Base,
+ timeZone: string
+): number {
+ switch (esUnit) {
+ case 's':
+ return 1000 * esValue;
+ case 'ms':
+ return 1 * esValue;
+ default:
+ return findIntervalFromDuration(value, esValue, esUnit, timeZone);
+ }
+}
+
+export function findMinInterval(
+ xValues: number[],
+ esValue: number,
+ esUnit: string,
+ timeZone: string
+): number {
+ return xValues.reduce((minInterval, currentXvalue, index) => {
+ let currentDiff = minInterval;
+ if (index > 0) {
+ currentDiff = Math.abs(xValues[index - 1] - currentXvalue);
+ }
+ const singleUnitInterval = getIntervalInMs(
+ currentXvalue,
+ esValue,
+ esUnit as unitOfTime.Base,
+ timeZone
+ );
+ return Math.min(minInterval, singleUnitInterval, currentDiff);
+ }, Number.MAX_SAFE_INTEGER);
+}
+
export class DiscoverHistogram extends Component {
public static propTypes = {
chartData: PropTypes.object,
@@ -154,7 +207,7 @@ export class DiscoverHistogram extends Component {
requestData();
- }, []);
+ });
return [status, hit, indexPattern];
}
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_field_search.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_field_search.tsx
index d5f6b63d12199..2910ff2825fe7 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_field_search.tsx
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_field_search.tsx
@@ -67,11 +67,6 @@ export interface Props {
* Additionally there's a button displayed that allows the user to show/hide more filter fields
*/
export function DiscoverFieldSearch({ onChange, value, types }: Props) {
- if (typeof value !== 'string') {
- // at initial rendering value is undefined (angular related), this catches the warning
- // should be removed once all is react
- return null;
- }
const searchPlaceholder = i18n.translate('kbn.discover.fieldChooser.searchPlaceHolder', {
defaultMessage: 'Search field names',
});
@@ -99,6 +94,12 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) {
missing: true,
});
+ if (typeof value !== 'string') {
+ // at initial rendering value is undefined (angular related), this catches the warning
+ // should be removed once all is react
+ return null;
+ }
+
const filterBtnAriaLabel = isPopoverOpen
? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', {
defaultMessage: 'Hide field filter settings',
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx
index 37338decce2c2..a4e8ee2ca3d8a 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern.tsx
@@ -45,19 +45,19 @@ export function DiscoverIndexPattern({
selectedIndexPattern,
setIndexPattern,
}: DiscoverIndexPatternProps) {
- if (!indexPatternList || indexPatternList.length === 0 || !selectedIndexPattern) {
- // just in case, shouldn't happen
- return null;
- }
- const options: IndexPatternRef[] = indexPatternList.map(entity => ({
+ const options: IndexPatternRef[] = (indexPatternList || []).map(entity => ({
id: entity.id,
title: entity.attributes!.title,
}));
+ const { id: selectedId, attributes } = selectedIndexPattern || {};
const [selected, setSelected] = useState({
- id: selectedIndexPattern.id,
- title: selectedIndexPattern.attributes!.title,
+ id: selectedId,
+ title: attributes?.title || '',
});
+ if (!selectedId) {
+ return null;
+ }
return (
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern_directive.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern_directive.tsx
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern_directive.ts
rename to src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern_directive.tsx
index 8bbeac086f093..d6527b0d7beed 100644
--- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern_directive.ts
+++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/discover_index_pattern_directive.tsx
@@ -16,11 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
+import React from 'react';
import { wrapInI18nContext } from '../../../kibana_services';
-import { DiscoverIndexPattern } from './discover_index_pattern';
+import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern';
+
+/**
+ * At initial rendering the angular directive the selectedIndexPattern prop is undefined
+ * This wrapper catches this, had to be introduced to satisfy eslint
+ */
+export function DiscoverIndexPatternWrapper(props: DiscoverIndexPatternProps) {
+ if (!props.selectedIndexPattern || !Array.isArray(props.indexPatternList)) {
+ return null;
+ }
+ return ;
+}
export function createIndexPatternSelectDirective(reactDirective: any) {
- return reactDirective(wrapInI18nContext(DiscoverIndexPattern), [
+ return reactDirective(wrapInI18nContext(DiscoverIndexPatternWrapper), [
['indexPatternList', { watchDepth: 'reference' }],
['selectedIndexPattern', { watchDepth: 'reference' }],
['setIndexPattern', { watchDepth: 'reference' }],
diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg
new file mode 100644
index 0000000000000..5a1d6e9a52f17
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js
index 5323fb2dac2d2..d62770956b88e 100644
--- a/src/legacy/core_plugins/kibana/public/management/index.js
+++ b/src/legacy/core_plugins/kibana/public/management/index.js
@@ -28,7 +28,8 @@ import { I18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
import appTemplate from './app.html';
import landingTemplate from './landing.html';
-import { management, SidebarNav, MANAGEMENT_BREADCRUMB } from 'ui/management';
+import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
+import { ManagementSidebarNav } from '../../../../../plugins/management/public';
import {
FeatureCatalogueRegistryProvider,
FeatureCatalogueCategory,
@@ -42,6 +43,7 @@ import {
EuiIcon,
EuiHorizontalRule,
} from '@elastic/eui';
+import { npStart } from 'ui/new_platform';
const SIDENAV_ID = 'management-sidenav';
const LANDING_ID = 'management-landing';
@@ -102,7 +104,7 @@ export function updateLandingPage(version) {
);
}
-export function updateSidebar(items, id) {
+export function updateSidebar(legacySections, id) {
const node = document.getElementById(SIDENAV_ID);
if (!node) {
return;
@@ -110,7 +112,12 @@ export function updateSidebar(items, id) {
render(
-
+ ,
node
);
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js
index 0909b6947895c..f7e654fd3c76d 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js
@@ -29,7 +29,7 @@ import { uiModules } from 'ui/modules';
import { fatalError, toastNotifications } from 'ui/notify';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
-import { isNumeric } from 'ui/utils/numeric';
+import { isNumeric } from './lib/numeric';
import { canViewInApp } from './lib/in_app_url';
import { castEsToKbnFieldTypeName } from '../../../../../../../plugins/data/public';
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts
new file mode 100644
index 0000000000000..bb749de8dcb71
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.test.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { keysToCamelCaseShallow } from './case_conversion';
+
+describe('keysToCamelCaseShallow', () => {
+ test("should convert all of an object's keys to camel case", () => {
+ const data = {
+ camelCase: 'camelCase',
+ 'kebab-case': 'kebabCase',
+ snake_case: 'snakeCase',
+ };
+
+ const result = keysToCamelCaseShallow(data);
+
+ expect(result.camelCase).toBe('camelCase');
+ expect(result.kebabCase).toBe('kebabCase');
+ expect(result.snakeCase).toBe('snakeCase');
+ });
+});
diff --git a/src/legacy/utils/case_conversion.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts
similarity index 77%
rename from src/legacy/utils/case_conversion.ts
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts
index e1c1317b26b14..718530eb3b602 100644
--- a/src/legacy/utils/case_conversion.ts
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/case_conversion.ts
@@ -17,16 +17,8 @@
* under the License.
*/
-import _ from 'lodash';
-
-export function keysToSnakeCaseShallow(object: Record) {
- return _.mapKeys(object, (value, key) => {
- return _.snakeCase(key);
- });
-}
+import { mapKeys, camelCase } from 'lodash';
export function keysToCamelCaseShallow(object: Record) {
- return _.mapKeys(object, (value, key) => {
- return _.camelCase(key);
- });
+ return mapKeys(object, (value, key) => camelCase(key));
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js
index f982966d1e314..caf2b5f503440 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/find_objects.js
@@ -18,7 +18,7 @@
*/
import { kfetch } from 'ui/kfetch';
-import { keysToCamelCaseShallow } from 'ui/utils/case_conversion';
+import { keysToCamelCaseShallow } from './case_conversion';
export async function findObjects(findOptions) {
const response = await kfetch({
@@ -26,5 +26,6 @@ export async function findObjects(findOptions) {
pathname: '/api/kibana/management/saved_objects/_find',
query: findOptions,
});
+
return keysToCamelCaseShallow(response);
}
diff --git a/src/legacy/ui/public/utils/numeric.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts
similarity index 86%
rename from src/legacy/ui/public/utils/numeric.ts
rename to src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts
index 1342498cf5dc3..c7bc6c26a378f 100644
--- a/src/legacy/ui/public/utils/numeric.ts
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/numeric.ts
@@ -17,8 +17,8 @@
* under the License.
*/
-import _ from 'lodash';
+import { isNaN } from 'lodash';
export function isNumeric(v: any): boolean {
- return !_.isNaN(v) && (typeof v === 'number' || (!Array.isArray(v) && !_.isNaN(parseFloat(v))));
+ return !isNaN(v) && (typeof v === 'number' || (!Array.isArray(v) && !isNaN(parseFloat(v))));
}
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js
index 0155b10f60218..c64b332e8ebee 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js
@@ -17,9 +17,10 @@
* under the License.
*/
-import { StringUtils } from 'ui/utils/string_utils';
import { i18n } from '@kbn/i18n';
+const upperFirst = (str = '') => str.replace(/^./, str => str.toUpperCase());
+
const names = {
general: i18n.translate('kbn.management.settings.categoryNames.generalLabel', {
defaultMessage: 'General',
@@ -51,5 +52,5 @@ const names = {
};
export function getCategoryName(category) {
- return category ? names[category] || StringUtils.upperFirst(category) : '';
+ return category ? names[category] || upperFirst(category) : '';
}
diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js
index ecc2d1df8c388..53ec16c1ca593 100644
--- a/src/legacy/core_plugins/kibana/server/tutorials/register.js
+++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js
@@ -83,6 +83,7 @@ import { awsLogsSpecProvider } from './aws_logs';
import { activemqLogsSpecProvider } from './activemq_logs';
import { activemqMetricsSpecProvider } from './activemq_metrics';
import { azureMetricsSpecProvider } from './azure_metrics';
+import { stanMetricsSpecProvider } from './stan_metrics';
import { envoyproxyMetricsSpecProvider } from './envoyproxy_metrics';
export function registerTutorials(server) {
@@ -155,5 +156,6 @@ export function registerTutorials(server) {
server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqLogsSpecProvider);
server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider);
server.newPlatform.setup.plugins.home.tutorials.registerTutorial(azureMetricsSpecProvider);
+ server.newPlatform.setup.plugins.home.tutorials.registerTutorial(stanMetricsSpecProvider);
server.newPlatform.setup.plugins.home.tutorials.registerTutorial(envoyproxyMetricsSpecProvider);
}
diff --git a/src/legacy/core_plugins/kibana/server/tutorials/stan_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/stan_metrics/index.js
new file mode 100644
index 0000000000000..3f5817ce2890b
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/server/tutorials/stan_metrics/index.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category';
+import {
+ onPremInstructions,
+ cloudInstructions,
+ onPremCloudInstructions,
+} from '../../../common/tutorials/metricbeat_instructions';
+
+export function stanMetricsSpecProvider(context) {
+ const moduleName = 'stan';
+ return {
+ id: 'stanMetrics',
+ name: i18n.translate('kbn.server.tutorials.stanMetrics.nameTitle', {
+ defaultMessage: 'STAN metrics',
+ }),
+ category: TUTORIAL_CATEGORY.METRICS,
+ shortDescription: i18n.translate('kbn.server.tutorials.stanMetrics.shortDescription', {
+ defaultMessage: 'Fetch monitoring metrics from the STAN server.',
+ }),
+ longDescription: i18n.translate('kbn.server.tutorials.stanMetrics.longDescription', {
+ defaultMessage:
+ 'The `stan` Metricbeat module fetches monitoring metrics from STAN. \
+[Learn more]({learnMoreLink}).',
+ values: {
+ learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-stan.html',
+ },
+ }),
+ euiIconType: '/plugins/kibana/home/tutorial_resources/logos/stan.svg',
+ artifacts: {
+ dashboards: [],
+ exportedFields: {
+ documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-stan.html',
+ },
+ },
+ completionTimeMinutes: 10,
+ // previewImagePath: '/plugins/kibana/home/tutorial_resources/stan_metrics/screenshot.png',
+ onPrem: onPremInstructions(moduleName, null, null, null, context),
+ elasticCloud: cloudInstructions(moduleName),
+ onPremElasticCloud: onPremCloudInstructions(moduleName),
+ };
+}
diff --git a/src/legacy/ui/public/utils/supports.js b/src/legacy/core_plugins/tile_map/public/css_filters.js
similarity index 64%
rename from src/legacy/ui/public/utils/supports.js
rename to src/legacy/core_plugins/tile_map/public/css_filters.js
index 19c5f34d97f36..63d6a358059b3 100644
--- a/src/legacy/ui/public/utils/supports.js
+++ b/src/legacy/core_plugins/tile_map/public/css_filters.js
@@ -22,22 +22,21 @@ import _ from 'lodash';
/**
* just a place to put feature detection checks
*/
-export const supports = {
- cssFilters: (function() {
- const e = document.createElement('img');
- const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter'];
- const test = 'grayscale(1)';
- rules.forEach(function(rule) {
- e.style[rule] = test;
- });
+export const supportsCssFilters = (function() {
+ const e = document.createElement('img');
+ const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter'];
+ const test = 'grayscale(1)';
- document.body.appendChild(e);
- const styles = window.getComputedStyle(e);
- const can = _(styles)
- .pick(rules)
- .includes(test);
- document.body.removeChild(e);
+ rules.forEach(function(rule) {
+ e.style[rule] = test;
+ });
- return can;
- })(),
-};
+ document.body.appendChild(e);
+ const styles = window.getComputedStyle(e);
+ const can = _(styles)
+ .pick(rules)
+ .includes(test);
+ document.body.removeChild(e);
+
+ return can;
+})();
diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js
index f2354614ac41a..f2e6469e768e7 100644
--- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js
+++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js
@@ -20,7 +20,6 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { supports } from 'ui/utils/supports';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { colorSchemas } from 'ui/vislib/components/color/truncated_colormaps';
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
@@ -29,6 +28,7 @@ import { createTileMapVisualization } from './tile_map_visualization';
import { Status } from '../../visualizations/public';
import { TileMapOptions } from './components/tile_map_options';
import { MapTypes } from './map_types';
+import { supportsCssFilters } from './css_filters';
export function createTileMapTypeDefinition(dependencies) {
const CoordinateMapsVisualization = createTileMapVisualization(dependencies);
@@ -44,7 +44,7 @@ export function createTileMapTypeDefinition(dependencies) {
defaultMessage: 'Plot latitude and longitude coordinates on a map',
}),
visConfig: {
- canDesaturate: !!supports.cssFilters,
+ canDesaturate: Boolean(supportsCssFilters),
defaults: {
colorSchema: 'Yellow to Red',
mapType: 'Scaled Circle Markers',
diff --git a/src/legacy/core_plugins/timelion/common/types.ts b/src/legacy/core_plugins/timelion/common/types.ts
new file mode 100644
index 0000000000000..f7084948a14f7
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/common/types.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null';
+
+interface TimelionFunctionArgsSuggestion {
+ name: string;
+ help: string;
+}
+
+export interface TimelionFunctionArgs {
+ name: string;
+ help?: string;
+ multi?: boolean;
+ types: TimelionFunctionArgsTypes[];
+ suggestions?: TimelionFunctionArgsSuggestion[];
+}
+
+export interface ITimelionFunction {
+ aliases: string[];
+ args: TimelionFunctionArgs[];
+ name: string;
+ help: string;
+ chainable: boolean;
+ extended: boolean;
+ isAlias: boolean;
+ argsByName: {
+ [key: string]: TimelionFunctionArgs[];
+ };
+}
diff --git a/src/legacy/core_plugins/timelion/public/components/_index.scss b/src/legacy/core_plugins/timelion/public/components/_index.scss
new file mode 100644
index 0000000000000..f2458a367e176
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/_index.scss
@@ -0,0 +1 @@
+@import './timelion_expression_input';
diff --git a/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss b/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss
new file mode 100644
index 0000000000000..b1c0b5514ff7a
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/_timelion_expression_input.scss
@@ -0,0 +1,18 @@
+.timExpressionInput {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ margin-top: $euiSize;
+}
+
+.timExpressionInput__editor {
+ height: 100%;
+ padding-top: $euiSizeS;
+}
+
+@include euiBreakpoint('xs', 's', 'm') {
+ .timExpressionInput__editor {
+ height: $euiSize * 15;
+ max-height: $euiSize * 15;
+ }
+}
diff --git a/src/legacy/core_plugins/timelion/public/components/index.ts b/src/legacy/core_plugins/timelion/public/components/index.ts
new file mode 100644
index 0000000000000..8d7d32a3ba262
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './timelion_expression_input';
+export * from './timelion_interval';
diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx
new file mode 100644
index 0000000000000..c695d09ca822b
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input.tsx
@@ -0,0 +1,146 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useCallback, useRef, useMemo } from 'react';
+import { EuiFormLabel } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
+
+import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public';
+import { suggest, getSuggestion } from './timelion_expression_input_helpers';
+import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
+import { getArgValueSuggestions } from '../services/arg_value_suggestions';
+
+const LANGUAGE_ID = 'timelion_expression';
+monacoEditor.languages.register({ id: LANGUAGE_ID });
+
+interface TimelionExpressionInputProps {
+ value: string;
+ setValue(value: string): void;
+}
+
+function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputProps) {
+ const functionList = useRef([]);
+ const kibana = useKibana();
+ const argValueSuggestions = useMemo(getArgValueSuggestions, []);
+
+ const provideCompletionItems = useCallback(
+ async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
+ const text = model.getValue();
+ const wordUntil = model.getWordUntilPosition(position);
+ const wordRange = new monacoEditor.Range(
+ position.lineNumber,
+ wordUntil.startColumn,
+ position.lineNumber,
+ wordUntil.endColumn
+ );
+
+ const suggestions = await suggest(
+ text,
+ functionList.current,
+ // it's important to offset the cursor position on 1 point left
+ // because of PEG parser starts the line with 0, but monaco with 1
+ position.column - 1,
+ argValueSuggestions
+ );
+
+ return {
+ suggestions: suggestions
+ ? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) =>
+ getSuggestion(s, suggestions.type, wordRange)
+ )
+ : [],
+ };
+ },
+ [argValueSuggestions]
+ );
+
+ const provideHover = useCallback(
+ async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
+ const suggestions = await suggest(
+ model.getValue(),
+ functionList.current,
+ // it's important to offset the cursor position on 1 point left
+ // because of PEG parser starts the line with 0, but monaco with 1
+ position.column - 1,
+ argValueSuggestions
+ );
+
+ return {
+ contents: suggestions
+ ? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) => ({
+ value: s.help,
+ }))
+ : [],
+ };
+ },
+ [argValueSuggestions]
+ );
+
+ useEffect(() => {
+ if (kibana.services.http) {
+ kibana.services.http.get('../api/timelion/functions').then(data => {
+ functionList.current = data;
+ });
+ }
+ }, [kibana.services.http]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export { TimelionExpressionInput };
diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts
new file mode 100644
index 0000000000000..fc90c276eeca2
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/timelion_expression_input_helpers.ts
@@ -0,0 +1,287 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { get, startsWith } from 'lodash';
+import PEG from 'pegjs';
+import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
+
+// @ts-ignore
+import grammar from 'raw-loader!../chain.peg';
+
+import { i18n } from '@kbn/i18n';
+import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
+import { ArgValueSuggestions, FunctionArg, Location } from '../services/arg_value_suggestions';
+
+const Parser = PEG.generate(grammar);
+
+export enum SUGGESTION_TYPE {
+ ARGUMENTS = 'arguments',
+ ARGUMENT_VALUE = 'argument_value',
+ FUNCTIONS = 'functions',
+}
+
+function inLocation(cursorPosition: number, location: Location) {
+ return cursorPosition >= location.min && cursorPosition <= location.max;
+}
+
+function getArgumentsHelp(
+ functionHelp: ITimelionFunction | undefined,
+ functionArgs: FunctionArg[] = []
+) {
+ if (!functionHelp) {
+ return [];
+ }
+
+ // Do not provide 'inputSeries' as argument suggestion for chainable functions
+ const argsHelp = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0);
+
+ // ignore arguments that are already provided in function declaration
+ const functionArgNames = functionArgs.map(arg => arg.name);
+ return argsHelp.filter(arg => !functionArgNames.includes(arg.name));
+}
+
+async function extractSuggestionsFromParsedResult(
+ result: ReturnType,
+ cursorPosition: number,
+ functionList: ITimelionFunction[],
+ argValueSuggestions: ArgValueSuggestions
+) {
+ const activeFunc = result.functions.find(({ location }: { location: Location }) =>
+ inLocation(cursorPosition, location)
+ );
+
+ if (!activeFunc) {
+ return;
+ }
+
+ const functionHelp = functionList.find(({ name }) => name === activeFunc.function);
+
+ if (!functionHelp) {
+ return;
+ }
+
+ // return function suggestion when cursor is outside of parentheses
+ // location range includes '.', function name, and '('.
+ const openParen = activeFunc.location.min + activeFunc.function.length + 2;
+ if (cursorPosition < openParen) {
+ return { list: [functionHelp], type: SUGGESTION_TYPE.FUNCTIONS };
+ }
+
+ // return argument value suggestions when cursor is inside argument value
+ const activeArg = activeFunc.arguments.find((argument: FunctionArg) => {
+ return inLocation(cursorPosition, argument.location);
+ });
+ if (
+ activeArg &&
+ activeArg.type === 'namedArg' &&
+ inLocation(cursorPosition, activeArg.value.location)
+ ) {
+ const { function: functionName, arguments: functionArgs } = activeFunc;
+
+ const {
+ name: argName,
+ value: { text: partialInput },
+ } = activeArg;
+
+ let valueSuggestions;
+ if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
+ valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
+ functionName,
+ argName,
+ functionArgs,
+ partialInput
+ );
+ } else {
+ const { suggestions: staticSuggestions } =
+ functionHelp.args.find(arg => arg.name === activeArg.name) || {};
+ valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput(
+ partialInput,
+ staticSuggestions
+ );
+ }
+ return {
+ list: valueSuggestions,
+ type: SUGGESTION_TYPE.ARGUMENT_VALUE,
+ };
+ }
+
+ // return argument suggestions
+ const argsHelp = getArgumentsHelp(functionHelp, activeFunc.arguments);
+ const argumentSuggestions = argsHelp.filter(arg => {
+ if (get(activeArg, 'type') === 'namedArg') {
+ return startsWith(arg.name, activeArg.name);
+ } else if (activeArg) {
+ return startsWith(arg.name, activeArg.text);
+ }
+ return true;
+ });
+ return { list: argumentSuggestions, type: SUGGESTION_TYPE.ARGUMENTS };
+}
+
+export async function suggest(
+ expression: string,
+ functionList: ITimelionFunction[],
+ cursorPosition: number,
+ argValueSuggestions: ArgValueSuggestions
+) {
+ try {
+ const result = await Parser.parse(expression);
+
+ return await extractSuggestionsFromParsedResult(
+ result,
+ cursorPosition,
+ functionList,
+ argValueSuggestions
+ );
+ } catch (err) {
+ let message: any;
+ try {
+ // The grammar will throw an error containing a message if the expression is formatted
+ // correctly and is prepared to accept suggestions. If the expression is not formatted
+ // correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
+ // attempt will throw an error.
+ message = JSON.parse(err.message);
+ } catch (e) {
+ // The expression isn't correctly formatted, so JSON.parse threw an error.
+ return;
+ }
+
+ switch (message.type) {
+ case 'incompleteFunction': {
+ let list;
+ if (message.function) {
+ // The user has start typing a function name, so we'll filter the list down to only
+ // possible matches.
+ list = functionList.filter(func => startsWith(func.name, message.function));
+ } else {
+ // The user hasn't typed anything yet, so we'll just return the entire list.
+ list = functionList;
+ }
+ return { list, type: SUGGESTION_TYPE.FUNCTIONS };
+ }
+ case 'incompleteArgument': {
+ const { currentFunction: functionName, currentArgs: functionArgs } = message;
+ const functionHelp = functionList.find(func => func.name === functionName);
+ return {
+ list: getArgumentsHelp(functionHelp, functionArgs),
+ type: SUGGESTION_TYPE.ARGUMENTS,
+ };
+ }
+ case 'incompleteArgumentValue': {
+ const { name: argName, currentFunction: functionName, currentArgs: functionArgs } = message;
+ let valueSuggestions = [];
+ if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
+ valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
+ functionName,
+ argName,
+ functionArgs
+ );
+ } else {
+ const functionHelp = functionList.find(func => func.name === functionName);
+ if (functionHelp) {
+ const argHelp = functionHelp.args.find(arg => arg.name === argName);
+ if (argHelp && argHelp.suggestions) {
+ valueSuggestions = argHelp.suggestions;
+ }
+ }
+ }
+ return {
+ list: valueSuggestions,
+ type: SUGGESTION_TYPE.ARGUMENT_VALUE,
+ };
+ }
+ }
+ }
+}
+
+export function getSuggestion(
+ suggestion: ITimelionFunction | TimelionFunctionArgs,
+ type: SUGGESTION_TYPE,
+ range: monacoEditor.Range
+): monacoEditor.languages.CompletionItem {
+ let kind: monacoEditor.languages.CompletionItemKind =
+ monacoEditor.languages.CompletionItemKind.Method;
+ let insertText: string = suggestion.name;
+ let insertTextRules: monacoEditor.languages.CompletionItem['insertTextRules'];
+ let detail: string = '';
+ let command: monacoEditor.languages.CompletionItem['command'];
+
+ switch (type) {
+ case SUGGESTION_TYPE.ARGUMENTS:
+ command = {
+ title: 'Trigger Suggestion Dialog',
+ id: 'editor.action.triggerSuggest',
+ };
+ kind = monacoEditor.languages.CompletionItemKind.Property;
+ insertText = `${insertText}=`;
+ detail = `${i18n.translate(
+ 'timelion.expressionSuggestions.argument.description.acceptsText',
+ {
+ defaultMessage: 'Accepts',
+ }
+ )}: ${(suggestion as TimelionFunctionArgs).types}`;
+
+ break;
+ case SUGGESTION_TYPE.FUNCTIONS:
+ command = {
+ title: 'Trigger Suggestion Dialog',
+ id: 'editor.action.triggerSuggest',
+ };
+ kind = monacoEditor.languages.CompletionItemKind.Function;
+ insertText = `${insertText}($0)`;
+ insertTextRules = monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet;
+ detail = `(${
+ (suggestion as ITimelionFunction).chainable
+ ? i18n.translate('timelion.expressionSuggestions.func.description.chainableHelpText', {
+ defaultMessage: 'Chainable',
+ })
+ : i18n.translate('timelion.expressionSuggestions.func.description.dataSourceHelpText', {
+ defaultMessage: 'Data source',
+ })
+ })`;
+
+ break;
+ case SUGGESTION_TYPE.ARGUMENT_VALUE:
+ const param = suggestion.name.split(':');
+
+ if (param.length === 1 || param[1]) {
+ insertText = `${param.length === 1 ? insertText : param[1]},`;
+ }
+
+ command = {
+ title: 'Trigger Suggestion Dialog',
+ id: 'editor.action.triggerSuggest',
+ };
+ kind = monacoEditor.languages.CompletionItemKind.Property;
+ detail = suggestion.help || '';
+
+ break;
+ }
+
+ return {
+ detail,
+ insertText,
+ insertTextRules,
+ kind,
+ label: suggestion.name,
+ documentation: suggestion.help,
+ command,
+ range,
+ };
+}
diff --git a/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx b/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx
new file mode 100644
index 0000000000000..6294e51e54788
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/components/timelion_interval.tsx
@@ -0,0 +1,144 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useMemo, useCallback } from 'react';
+import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { useValidation } from 'ui/vis/editors/default/controls/agg_utils';
+import { isValidEsInterval } from '../../../../core_plugins/data/common';
+
+const intervalOptions = [
+ {
+ label: i18n.translate('timelion.vis.interval.auto', {
+ defaultMessage: 'Auto',
+ }),
+ value: 'auto',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.second', {
+ defaultMessage: '1 second',
+ }),
+ value: '1s',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.minute', {
+ defaultMessage: '1 minute',
+ }),
+ value: '1m',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.hour', {
+ defaultMessage: '1 hour',
+ }),
+ value: '1h',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.day', {
+ defaultMessage: '1 day',
+ }),
+ value: '1d',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.week', {
+ defaultMessage: '1 week',
+ }),
+ value: '1w',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.month', {
+ defaultMessage: '1 month',
+ }),
+ value: '1M',
+ },
+ {
+ label: i18n.translate('timelion.vis.interval.year', {
+ defaultMessage: '1 year',
+ }),
+ value: '1y',
+ },
+];
+
+interface TimelionIntervalProps {
+ value: string;
+ setValue(value: string): void;
+ setValidity(valid: boolean): void;
+}
+
+function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProps) {
+ const onCustomInterval = useCallback(
+ (customValue: string) => {
+ setValue(customValue.trim());
+ },
+ [setValue]
+ );
+
+ const onChange = useCallback(
+ (opts: Array>) => {
+ setValue((opts[0] && opts[0].value) || '');
+ },
+ [setValue]
+ );
+
+ const selectedOptions = useMemo(
+ () => [intervalOptions.find(op => op.value === value) || { label: value, value }],
+ [value]
+ );
+
+ const isValid = intervalOptions.some(int => int.value === value) || isValidEsInterval(value);
+
+ useValidation(setValidity, isValid);
+
+ return (
+
+
+
+ );
+}
+
+export { TimelionInterval };
diff --git a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js
index b90f5932b5b09..231330b898edb 100644
--- a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js
+++ b/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js
@@ -21,9 +21,15 @@ import expect from '@kbn/expect';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../chain.peg';
import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers';
-import { ArgValueSuggestionsProvider } from '../timelion_expression_suggestions/arg_value_suggestions';
+import { getArgValueSuggestions } from '../../services/arg_value_suggestions';
+import { setIndexPatterns, setSavedObjectsClient } from '../../services/plugin_services';
describe('Timelion expression suggestions', () => {
+ setIndexPatterns({});
+ setSavedObjectsClient({});
+
+ const argValueSuggestions = getArgValueSuggestions();
+
describe('getSuggestions', () => {
const func1 = {
name: 'func1',
@@ -44,11 +50,6 @@ describe('Timelion expression suggestions', () => {
};
const functionList = [func1, myFunc2];
let Parser;
- const privateStub = () => {
- return {};
- };
- const indexPatternsStub = {};
- const argValueSuggestions = ArgValueSuggestionsProvider(privateStub, indexPatternsStub); // eslint-disable-line new-cap
beforeEach(function() {
Parser = PEG.generate(grammar);
});
diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
index c0092ca49c4f3..111db0a83ffc4 100644
--- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
+++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
@@ -19,12 +19,12 @@
import _ from 'lodash';
import rison from 'rison-node';
-import { keyMap } from 'ui/utils/key_map';
import { uiModules } from 'ui/modules';
import 'ui/directives/input_focus';
import 'ui/directives/paginate';
import savedObjectFinderTemplate from './saved_object_finder.html';
import { savedSheetLoader } from '../services/saved_sheets';
+import { keyMap } from 'ui/directives/key_map';
const module = uiModules.get('kibana');
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
index 137dd6b82046d..449c0489fea25 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
+++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
@@ -52,11 +52,11 @@ import {
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from '@elastic/eui';
-import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions';
+import { getArgValueSuggestions } from '../services/arg_value_suggestions';
const Parser = PEG.generate(grammar);
-export function TimelionExpInput($http, $timeout, Private) {
+export function TimelionExpInput($http, $timeout) {
return {
restrict: 'E',
scope: {
@@ -68,7 +68,7 @@ export function TimelionExpInput($http, $timeout, Private) {
replace: true,
template: timelionExpressionInputTemplate,
link: function(scope, elem) {
- const argValueSuggestions = Private(ArgValueSuggestionsProvider);
+ const argValueSuggestions = getArgValueSuggestions();
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};
diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/legacy/core_plugins/timelion/public/index.scss
index f6123f4052156..7ccc6c300bc40 100644
--- a/src/legacy/core_plugins/timelion/public/index.scss
+++ b/src/legacy/core_plugins/timelion/public/index.scss
@@ -11,5 +11,6 @@
// timChart__legend-isLoading
@import './app';
+@import './components/index';
@import './directives/index';
@import './vis/index';
diff --git a/src/legacy/core_plugins/timelion/public/legacy.ts b/src/legacy/core_plugins/timelion/public/legacy.ts
index d989a68d40eeb..1cf6bb65cdc02 100644
--- a/src/legacy/core_plugins/timelion/public/legacy.ts
+++ b/src/legacy/core_plugins/timelion/public/legacy.ts
@@ -37,4 +37,4 @@ const setupPlugins: Readonly = {
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
-export const start = pluginInstance.start(npStart.core);
+export const start = pluginInstance.start(npStart.core, npStart.plugins);
diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
index 04b27c4020ce3..0bbda4bf3646f 100644
--- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
+++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
@@ -35,6 +35,7 @@ const DEBOUNCE_DELAY = 50;
export function timechartFn(dependencies: TimelionVisualizationDependencies) {
const { $rootScope, $compile, uiSettings } = dependencies;
+
return function() {
return {
help: 'Draw a timeseries chart',
diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts
index ba8c25c20abea..42f0ee3ad4725 100644
--- a/src/legacy/core_plugins/timelion/public/plugin.ts
+++ b/src/legacy/core_plugins/timelion/public/plugin.ts
@@ -26,12 +26,14 @@ import {
} from 'kibana/public';
import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public';
import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public';
+import { PluginsStart } from 'ui/new_platform/new_platform';
import { VisualizationsSetup } from '../../visualizations/public/np_ready/public';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisualization } from './vis';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim';
+import { setIndexPatterns, setSavedObjectsClient } from './services/plugin_services';
/** @internal */
export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup {
@@ -85,12 +87,15 @@ export class TimelionPlugin implements Plugin, void> {
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
- public start(core: CoreStart) {
+ public start(core: CoreStart, plugins: PluginsStart) {
const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled');
if (timelionUiEnabled === false) {
core.chrome.navLinks.update('timelion', { hidden: true });
}
+
+ setIndexPatterns(plugins.data.indexPatterns);
+ setSavedObjectsClient(core.savedObjects.client);
}
public stop(): void {}
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js b/src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts
similarity index 72%
rename from src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js
rename to src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts
index e698a69401a37..8d133de51f6d9 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js
+++ b/src/legacy/core_plugins/timelion/public/services/arg_value_suggestions.ts
@@ -17,33 +17,51 @@
* under the License.
*/
-import _ from 'lodash';
-import { npStart } from 'ui/new_platform';
+import { get } from 'lodash';
+import { TimelionFunctionArgs } from '../../common/types';
+import { getIndexPatterns, getSavedObjectsClient } from './plugin_services';
-export function ArgValueSuggestionsProvider() {
- const { indexPatterns } = npStart.plugins.data;
- const { client: savedObjectsClient } = npStart.core.savedObjects;
+export interface Location {
+ min: number;
+ max: number;
+}
- async function getIndexPattern(functionArgs) {
- const indexPatternArg = functionArgs.find(argument => {
- return argument.name === 'index';
- });
+export interface FunctionArg {
+ function: string;
+ location: Location;
+ name: string;
+ text: string;
+ type: string;
+ value: {
+ location: Location;
+ text: string;
+ type: string;
+ value: string;
+ };
+}
+
+export function getArgValueSuggestions() {
+ const indexPatterns = getIndexPatterns();
+ const savedObjectsClient = getSavedObjectsClient();
+
+ async function getIndexPattern(functionArgs: FunctionArg[]) {
+ const indexPatternArg = functionArgs.find(({ name }) => name === 'index');
if (!indexPatternArg) {
// index argument not provided
return;
}
- const indexPatternTitle = _.get(indexPatternArg, 'value.text');
+ const indexPatternTitle = get(indexPatternArg, 'value.text');
- const resp = await savedObjectsClient.find({
+ const { savedObjects } = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title'],
search: `"${indexPatternTitle}"`,
- search_fields: ['title'],
+ searchFields: ['title'],
perPage: 10,
});
- const indexPatternSavedObject = resp.savedObjects.find(savedObject => {
- return savedObject.attributes.title === indexPatternTitle;
- });
+ const indexPatternSavedObject = savedObjects.find(
+ ({ attributes }) => attributes.title === indexPatternTitle
+ );
if (!indexPatternSavedObject) {
// index argument does not match an index pattern
return;
@@ -52,7 +70,7 @@ export function ArgValueSuggestionsProvider() {
return await indexPatterns.get(indexPatternSavedObject.id);
}
- function containsFieldName(partial, field) {
+ function containsFieldName(partial: string, field: { name: string }) {
if (!partial) {
return true;
}
@@ -63,13 +81,13 @@ export function ArgValueSuggestionsProvider() {
// Could not put with function definition since functions are defined on server
const customHandlers = {
es: {
- index: async function(partial) {
+ async index(partial: string) {
const search = partial ? `${partial}*` : '*';
const resp = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title', 'type'],
search: `${search}`,
- search_fields: ['title'],
+ searchFields: ['title'],
perPage: 25,
});
return resp.savedObjects
@@ -78,7 +96,7 @@ export function ArgValueSuggestionsProvider() {
return { name: savedObject.attributes.title };
});
},
- metric: async function(partial, functionArgs) {
+ async metric(partial: string, functionArgs: FunctionArg[]) {
if (!partial || !partial.includes(':')) {
return [
{ name: 'avg:' },
@@ -109,7 +127,7 @@ export function ArgValueSuggestionsProvider() {
return { name: `${valueSplit[0]}:${field.name}`, help: field.type };
});
},
- split: async function(partial, functionArgs) {
+ async split(partial: string, functionArgs: FunctionArg[]) {
const indexPattern = await getIndexPattern(functionArgs);
if (!indexPattern) {
return [];
@@ -127,7 +145,7 @@ export function ArgValueSuggestionsProvider() {
return { name: field.name, help: field.type };
});
},
- timefield: async function(partial, functionArgs) {
+ async timefield(partial: string, functionArgs: FunctionArg[]) {
const indexPattern = await getIndexPattern(functionArgs);
if (!indexPattern) {
return [];
@@ -150,7 +168,10 @@ export function ArgValueSuggestionsProvider() {
* @param {string} argName - user provided argument name
* @return {boolean} true when dynamic suggestion handler provided for function argument
*/
- hasDynamicSuggestionsForArgument: (functionName, argName) => {
+ hasDynamicSuggestionsForArgument: (
+ functionName: T,
+ argName: keyof typeof customHandlers[T]
+ ) => {
return customHandlers[functionName] && customHandlers[functionName][argName];
},
@@ -161,12 +182,13 @@ export function ArgValueSuggestionsProvider() {
* @param {string} partial - user provided argument value
* @return {array} array of dynamic suggestions matching partial
*/
- getDynamicSuggestionsForArgument: async (
- functionName,
- argName,
- functionArgs,
+ getDynamicSuggestionsForArgument: async (
+ functionName: T,
+ argName: keyof typeof customHandlers[T],
+ functionArgs: FunctionArg[],
partialInput = ''
) => {
+ // @ts-ignore
return await customHandlers[functionName][argName](partialInput, functionArgs);
},
@@ -175,7 +197,10 @@ export function ArgValueSuggestionsProvider() {
* @param {array} staticSuggestions - argument value suggestions
* @return {array} array of static suggestions matching partial
*/
- getStaticSuggestionsForInput: (partialInput = '', staticSuggestions = []) => {
+ getStaticSuggestionsForInput: (
+ partialInput = '',
+ staticSuggestions: TimelionFunctionArgs['suggestions'] = []
+ ) => {
if (partialInput) {
return staticSuggestions.filter(suggestion => {
return suggestion.name.includes(partialInput);
@@ -186,3 +211,5 @@ export function ArgValueSuggestionsProvider() {
},
};
}
+
+export type ArgValueSuggestions = ReturnType;
diff --git a/src/legacy/ui/public/utils/sort_prefix_first.ts b/src/legacy/core_plugins/timelion/public/services/plugin_services.ts
similarity index 63%
rename from src/legacy/ui/public/utils/sort_prefix_first.ts
rename to src/legacy/core_plugins/timelion/public/services/plugin_services.ts
index 4d1a8d7f39866..5ba4ee5e47983 100644
--- a/src/legacy/ui/public/utils/sort_prefix_first.ts
+++ b/src/legacy/core_plugins/timelion/public/services/plugin_services.ts
@@ -17,17 +17,14 @@
* under the License.
*/
-import { partition } from 'lodash';
+import { IndexPatternsContract } from 'src/plugins/data/public';
+import { SavedObjectsClientContract } from 'kibana/public';
+import { createGetterSetter } from '../../../../../plugins/kibana_utils/public';
-export function sortPrefixFirst(array: any[], prefix?: string | number, property?: string): any[] {
- if (!prefix) {
- return array;
- }
- const lowerCasePrefix = ('' + prefix).toLowerCase();
+export const [getIndexPatterns, setIndexPatterns] = createGetterSetter(
+ 'IndexPatterns'
+);
- const partitions = partition(array, entry => {
- const value = ('' + (property ? entry[property] : entry)).toLowerCase();
- return value.startsWith(lowerCasePrefix);
- });
- return [...partitions[0], ...partitions[1]];
-}
+export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter<
+ SavedObjectsClientContract
+>('SavedObjectsClient');
diff --git a/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts b/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts
index 474f464a550cd..206f9f5d8368d 100644
--- a/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts
+++ b/src/legacy/core_plugins/timelion/public/timelion_vis_fn.ts
@@ -28,7 +28,7 @@ const name = 'timelion_vis';
interface Arguments {
expression: string;
- interval: any;
+ interval: string;
}
interface RenderValue {
@@ -38,7 +38,7 @@ interface RenderValue {
}
type Context = KibanaContext | null;
-type VisParams = Arguments;
+export type VisParams = Arguments;
type Return = Promise>;
export const getTimelionVisualizationConfig = (
@@ -60,7 +60,7 @@ export const getTimelionVisualizationConfig = (
help: '',
},
interval: {
- types: ['string', 'null'],
+ types: ['string'],
default: 'auto',
help: '',
},
diff --git a/src/legacy/core_plugins/timelion/public/vis/_index.scss b/src/legacy/core_plugins/timelion/public/vis/_index.scss
index e44b6336d33c1..17a2018f7a56a 100644
--- a/src/legacy/core_plugins/timelion/public/vis/_index.scss
+++ b/src/legacy/core_plugins/timelion/public/vis/_index.scss
@@ -1 +1,2 @@
@import './timelion_vis';
+@import './timelion_editor';
diff --git a/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss b/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss
new file mode 100644
index 0000000000000..a9331930a86ff
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/vis/_timelion_editor.scss
@@ -0,0 +1,15 @@
+.visEditor--timelion {
+ vis-options-react-wrapper,
+ .visEditorSidebar__options,
+ .visEditorSidebar__timelionOptions {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .visEditor__sidebar {
+ @include euiBreakpoint('xs', 's', 'm') {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/legacy/core_plugins/timelion/public/vis/index.ts b/src/legacy/core_plugins/timelion/public/vis/index.tsx
similarity index 80%
rename from src/legacy/core_plugins/timelion/public/vis/index.ts
rename to src/legacy/core_plugins/timelion/public/vis/index.tsx
index 7b82553a24e5b..1edcb0a5ce71c 100644
--- a/src/legacy/core_plugins/timelion/public/vis/index.ts
+++ b/src/legacy/core_plugins/timelion/public/vis/index.tsx
@@ -17,19 +17,24 @@
* under the License.
*/
+import React from 'react';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { DefaultEditorSize } from 'ui/vis/editor_size';
+import { VisOptionsProps } from 'ui/vis/editors/default';
+import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public';
import { getTimelionRequestHandler } from './timelion_request_handler';
import visConfigTemplate from './timelion_vis.html';
-import editorConfigTemplate from './timelion_vis_params.html';
import { TimelionVisualizationDependencies } from '../plugin';
// @ts-ignore
import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type';
+import { TimelionOptions } from './timelion_options';
+import { VisParams } from '../timelion_vis_fn';
export const TIMELION_VIS_NAME = 'timelion';
export function getTimelionVisualization(dependencies: TimelionVisualizationDependencies) {
+ const { http, uiSettings } = dependencies;
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
// return the visType object, which kibana will use to display and configure new
@@ -50,7 +55,11 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe
template: visConfigTemplate,
},
editorConfig: {
- optionsTemplate: editorConfigTemplate,
+ optionsTemplate: (props: VisOptionsProps) => (
+
+
+
+ ),
defaultSize: DefaultEditorSize.MEDIUM,
},
requestHandler: timelionRequestHandler,
diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx b/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx
new file mode 100644
index 0000000000000..527fcc3bc6ce8
--- /dev/null
+++ b/src/legacy/core_plugins/timelion/public/vis/timelion_options.tsx
@@ -0,0 +1,48 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useCallback } from 'react';
+import { EuiPanel } from '@elastic/eui';
+
+import { VisOptionsProps } from 'ui/vis/editors/default';
+import { VisParams } from '../timelion_vis_fn';
+import { TimelionInterval, TimelionExpressionInput } from '../components';
+
+function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps) {
+ const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [
+ setValue,
+ ]);
+ const setExpressionInput = useCallback(
+ (value: VisParams['expression']) => setValue('expression', value),
+ [setValue]
+ );
+
+ return (
+
+
+
+
+ );
+}
+
+export { TimelionOptions };
diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html b/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html
deleted file mode 100644
index 9f2d2094fb1f7..0000000000000
--- a/src/legacy/core_plugins/timelion/public/vis/timelion_vis_params.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts b/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts
index 6e32a4454e707..798902aa133de 100644
--- a/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts
+++ b/src/legacy/core_plugins/timelion/server/lib/classes/timelion_function.d.ts
@@ -17,6 +17,8 @@
* under the License.
*/
+import { TimelionFunctionArgs } from '../../../common/types';
+
export interface TimelionFunctionInterface extends TimelionFunctionConfig {
chainable: boolean;
originalFn: Function;
@@ -32,21 +34,6 @@ export interface TimelionFunctionConfig {
args: TimelionFunctionArgs[];
}
-export interface TimelionFunctionArgs {
- name: string;
- help?: string;
- multi?: boolean;
- types: TimelionFunctionArgsTypes[];
- suggestions?: TimelionFunctionArgsSuggestion[];
-}
-
-export type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null';
-
-export interface TimelionFunctionArgsSuggestion {
- name: string;
- help: string;
-}
-
// eslint-disable-next-line import/no-default-export
export default class TimelionFunction {
constructor(name: string, config: TimelionFunctionConfig);
diff --git a/src/legacy/core_plugins/timelion/server/types.ts b/src/legacy/core_plugins/timelion/server/types.ts
index e612bc14a0daa..a035d64f764f1 100644
--- a/src/legacy/core_plugins/timelion/server/types.ts
+++ b/src/legacy/core_plugins/timelion/server/types.ts
@@ -17,12 +17,5 @@
* under the License.
*/
-export {
- TimelionFunctionInterface,
- TimelionFunctionConfig,
- TimelionFunctionArgs,
- TimelionFunctionArgsSuggestion,
- TimelionFunctionArgsTypes,
-} from './lib/classes/timelion_function';
-
+export { TimelionFunctionInterface, TimelionFunctionConfig } from './lib/classes/timelion_function';
export { TimelionRequestQuery } from './routes/run';
diff --git a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js
index ca5ab20957281..0776dd13a9868 100644
--- a/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js
+++ b/src/legacy/core_plugins/vis_type_metric/public/components/metric_vis_value.js
@@ -21,13 +21,19 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { EuiKeyboardAccessible } from '@elastic/eui';
+import { EuiKeyboardAccessible, keyCodes } from '@elastic/eui';
class MetricVisValue extends Component {
onClick = () => {
this.props.onFilter(this.props.metric);
};
+ onKeyPress = e => {
+ if (e.keyCode === keyCodes.ENTER) {
+ this.onClick();
+ }
+ };
+
render() {
const { fontSize, metric, onFilter, showLabel } = this.props;
const hasFilter = !!onFilter;
@@ -47,6 +53,7 @@ class MetricVisValue extends Component {
className={containerClassName}
style={{ backgroundColor: metric.bgColor }}
onClick={hasFilter ? this.onClick : null}
+ onKeyPress={hasFilter ? this.onKeyPress : null}
tabIndex={hasFilter ? 0 : null}
role={hasFilter ? 'button' : null}
>
diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js
index 07870379265c5..3844f809a5257 100644
--- a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js
+++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js
@@ -135,18 +135,15 @@ export function KbnAggTable(config, RecursionHelper) {
}
const isDate =
- _.get(dimension, 'format.id') === 'date' ||
- _.get(dimension, 'format.params.id') === 'date';
- const isNumeric =
- _.get(dimension, 'format.id') === 'number' ||
- _.get(dimension, 'format.params.id') === 'number';
+ dimension.format?.id === 'date' || dimension.format?.params?.id === 'date';
+ const allowsNumericalAggregations = formatter?.allowsNumericalAggregations;
let { totalFunc } = $scope;
if (typeof totalFunc === 'undefined' && showPercentage) {
totalFunc = 'sum';
}
- if (isNumeric || isDate || totalFunc === 'count') {
+ if (allowsNumericalAggregations || isDate || totalFunc === 'count') {
const sum = tableRows => {
return _.reduce(
tableRows,
@@ -161,7 +158,6 @@ export function KbnAggTable(config, RecursionHelper) {
};
formattedColumn.sumTotal = sum(table.rows);
-
switch (totalFunc) {
case 'sum': {
if (!isDate) {
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js
index 982aca8d3b813..d269d7c3546ec 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js
@@ -79,10 +79,14 @@ export class TimeseriesVisualization extends Component {
static getYAxisDomain = model => {
const axisMin = get(model, 'axis_min', '').toString();
const axisMax = get(model, 'axis_max', '').toString();
+ const fit = model.series
+ ? model.series.filter(({ hidden }) => !hidden).every(({ fill }) => fill === '0')
+ : model.fill === '0';
return {
min: axisMin.length ? Number(axisMin) : undefined,
max: axisMax.length ? Number(axisMax) : undefined,
+ fit,
};
};
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js b/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js
index 46dab92f3b245..2dbcdc4749a66 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/lib/validate_interval.js
@@ -17,9 +17,9 @@
* under the License.
*/
-import { parseInterval } from 'ui/utils/parse_interval';
import { GTE_INTERVAL_RE } from '../../common/interval_regexp';
import { i18n } from '@kbn/i18n';
+import { parseInterval } from '../../../../../plugins/data/public';
export function validateInterval(bounds, panel, maxBuckets) {
const { interval } = panel;
diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx
index 71a88b47a8be3..3d7fda990b2ae 100644
--- a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx
+++ b/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx
@@ -34,12 +34,12 @@ function VegaActionsMenu({ formatHJson, formatJson }: VegaActionsMenuProps) {
const onHJsonCLick = useCallback(() => {
formatHJson();
setIsPopoverOpen(false);
- }, [isPopoverOpen, formatHJson]);
+ }, [formatHJson]);
const onJsonCLick = useCallback(() => {
formatJson();
setIsPopoverOpen(false);
- }, [isPopoverOpen, formatJson]);
+ }, [formatJson]);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts
index 59c6bddb64521..cc2ab133941db 100644
--- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts
+++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts
@@ -445,6 +445,8 @@ export const buildVislibDimensions = async (
dimensions.x.params.date = true;
const { esUnit, esValue } = xAgg.buckets.getInterval();
dimensions.x.params.interval = moment.duration(esValue, esUnit);
+ dimensions.x.params.intervalESValue = esValue;
+ dimensions.x.params.intervalESUnit = esUnit;
dimensions.x.params.format = xAgg.buckets.getScaledDateFormat();
dimensions.x.params.bounds = xAgg.buckets.getBounds();
} else if (xAgg.type.name === 'histogram') {
diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js
index a18cb7de5a61b..a53e8e0498c42 100644
--- a/src/legacy/server/config/schema.js
+++ b/src/legacy/server/config/schema.js
@@ -181,7 +181,7 @@ export default () =>
.default('localhost'),
watchPrebuild: Joi.boolean().default(false),
watchProxyTimeout: Joi.number().default(10 * 60000),
- useBundleCache: Joi.boolean().default(Joi.ref('$prod')),
+ useBundleCache: Joi.boolean().default(!!process.env.CODE_COVERAGE ? true : Joi.ref('$prod')),
sourceMaps: Joi.when('$prod', {
is: true,
then: Joi.boolean().valid(false),
diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts
index 0af6dacee59c8..8da1b3b05fa76 100644
--- a/src/legacy/server/kbn_server.d.ts
+++ b/src/legacy/server/kbn_server.d.ts
@@ -129,7 +129,7 @@ export interface KibanaCore {
plugins: PluginsSetup;
};
startDeps: {
- core: CoreSetup;
+ core: CoreStart;
plugins: Record;
};
logger: LoggerFactory;
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
index 0e96189db4650..691878cf66d27 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js
@@ -106,12 +106,7 @@ describe('Saved Objects Mixin', () => {
newPlatform: {
__internals: {
elasticsearch: {
- adminClient$: {
- pipe: jest.fn().mockImplementation(() => ({
- toPromise: () =>
- Promise.resolve({ adminClient: { callAsInternalUser: mockCallCluster } }),
- })),
- },
+ adminClient: { callAsInternalUser: mockCallCluster },
},
},
},
diff --git a/src/legacy/server/status/lib/case_conversion.test.ts b/src/legacy/server/status/lib/case_conversion.test.ts
new file mode 100644
index 0000000000000..a231ee0ba4b0f
--- /dev/null
+++ b/src/legacy/server/status/lib/case_conversion.test.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { keysToSnakeCaseShallow } from './case_conversion';
+
+describe('keysToSnakeCaseShallow', () => {
+ test("should convert all of an object's keys to snake case", () => {
+ const data = {
+ camelCase: 'camel_case',
+ 'kebab-case': 'kebab_case',
+ snake_case: 'snake_case',
+ };
+
+ const result = keysToSnakeCaseShallow(data);
+
+ expect(result.camel_case).toBe('camel_case');
+ expect(result.kebab_case).toBe('kebab_case');
+ expect(result.snake_case).toBe('snake_case');
+ });
+});
diff --git a/src/legacy/server/status/lib/case_conversion.ts b/src/legacy/server/status/lib/case_conversion.ts
new file mode 100644
index 0000000000000..a3ae15028daeb
--- /dev/null
+++ b/src/legacy/server/status/lib/case_conversion.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { mapKeys, snakeCase } from 'lodash';
+
+export function keysToSnakeCaseShallow(object: Record) {
+ return mapKeys(object, (value, key) => snakeCase(key));
+}
diff --git a/src/legacy/server/status/lib/metrics.js b/src/legacy/server/status/lib/metrics.js
index 322d8d8bd9ab4..2631b245e72ab 100644
--- a/src/legacy/server/status/lib/metrics.js
+++ b/src/legacy/server/status/lib/metrics.js
@@ -20,7 +20,7 @@
import os from 'os';
import v8 from 'v8';
import { get, isObject, merge } from 'lodash';
-import { keysToSnakeCaseShallow } from '../../../utils/case_conversion';
+import { keysToSnakeCaseShallow } from './case_conversion';
import { getAllStats as cGroupStats } from './cgroup';
import { getOSInfo } from './get_os_info';
diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss
index 98675402b43cc..747ad025ef691 100644
--- a/src/legacy/ui/public/_index.scss
+++ b/src/legacy/ui/public/_index.scss
@@ -20,6 +20,7 @@
@import './saved_objects/index';
@import './share/index';
@import './style_compile/index';
+@import '../../../plugins/management/public/components/index';
// The following are prefixed with "vis"
diff --git a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js b/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js
index 929aa4d5a7a9f..a8512edee658b 100644
--- a/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js
+++ b/src/legacy/ui/public/agg_response/point_series/__tests__/_init_x_axis.js
@@ -104,6 +104,8 @@ describe('initXAxis', function() {
it('reads the date interval param from the x agg', function() {
chart.aspects.x[0].params.interval = 'P1D';
+ chart.aspects.x[0].params.intervalESValue = 1;
+ chart.aspects.x[0].params.intervalESUnit = 'd';
chart.aspects.x[0].params.date = true;
initXAxis(chart, table);
expect(chart)
@@ -113,6 +115,8 @@ describe('initXAxis', function() {
expect(moment.isDuration(chart.ordered.interval)).to.be(true);
expect(chart.ordered.interval.toISOString()).to.eql('P1D');
+ expect(chart.ordered.intervalESValue).to.be(1);
+ expect(chart.ordered.intervalESUnit).to.be('d');
});
it('reads the numeric interval param from the x agg', function() {
diff --git a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js b/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js
index 531c564ea19d6..4a81486783b08 100644
--- a/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js
+++ b/src/legacy/ui/public/agg_response/point_series/_init_x_axis.js
@@ -28,9 +28,19 @@ export function initXAxis(chart, table) {
chart.xAxisFormat = format;
chart.xAxisLabel = title;
- if (params.interval) {
- chart.ordered = {
- interval: params.date ? moment.duration(params.interval) : params.interval,
- };
+ const { interval, date } = params;
+ if (interval) {
+ if (date) {
+ const { intervalESUnit, intervalESValue } = params;
+ chart.ordered = {
+ interval: moment.duration(interval),
+ intervalESUnit: intervalESUnit,
+ intervalESValue: intervalESValue,
+ };
+ } else {
+ chart.ordered = {
+ interval,
+ };
+ }
}
}
diff --git a/src/legacy/ui/public/utils/key_map.ts b/src/legacy/ui/public/directives/key_map.ts
similarity index 100%
rename from src/legacy/ui/public/utils/key_map.ts
rename to src/legacy/ui/public/directives/key_map.ts
diff --git a/src/legacy/ui/public/directives/watch_multi/watch_multi.js b/src/legacy/ui/public/directives/watch_multi/watch_multi.js
index 2a2ffe24cba9c..54b5cf08a9397 100644
--- a/src/legacy/ui/public/directives/watch_multi/watch_multi.js
+++ b/src/legacy/ui/public/directives/watch_multi/watch_multi.js
@@ -19,7 +19,6 @@
import _ from 'lodash';
import { uiModules } from '../../modules';
-import { callEach } from '../../utils/function';
export function watchMultiDecorator($provide) {
$provide.decorator('$rootScope', function($delegate) {
@@ -112,7 +111,9 @@ export function watchMultiDecorator($provide) {
)
);
- return _.partial(callEach, unwatchers);
+ return function() {
+ unwatchers.forEach(listener => listener());
+ };
};
function normalizeExpression($scope, expr) {
diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js
index 942f39fc98dec..12bf5c1cce004 100644
--- a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js
+++ b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js
@@ -30,6 +30,8 @@ import {
EuiTitle,
EuiCallOut,
} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { npStart } from 'ui/new_platform';
const { SearchBar } = npStart.plugins.data.ui;
@@ -116,7 +118,13 @@ export class TestScript extends Component {
if (previewData.error) {
return (
-
+
-
- Run your script to preview the first 10 results. You can also select some additional
- fields to include in your results to gain more context or add a query to filter on
- specific documents.
+
diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts
new file mode 100644
index 0000000000000..fc4ca8469382a
--- /dev/null
+++ b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { groupBy } from 'lodash';
+import { organizeBy } from './organize_by';
+
+describe('organizeBy', () => {
+ test('it works', () => {
+ const col = [
+ {
+ name: 'one',
+ roles: ['user', 'admin', 'owner'],
+ },
+ {
+ name: 'two',
+ roles: ['user'],
+ },
+ {
+ name: 'three',
+ roles: ['user'],
+ },
+ {
+ name: 'four',
+ roles: ['user', 'admin'],
+ },
+ ];
+
+ const resp = organizeBy(col, 'roles');
+ expect(resp).toHaveProperty('user');
+ expect(resp.user.length).toBe(4);
+
+ expect(resp).toHaveProperty('admin');
+ expect(resp.admin.length).toBe(2);
+
+ expect(resp).toHaveProperty('owner');
+ expect(resp.owner.length).toBe(1);
+ });
+
+ test('behaves just like groupBy in normal scenarios', () => {
+ const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }];
+
+ const orgs = organizeBy(col, 'name');
+ const groups = groupBy(col, 'name');
+
+ expect(orgs).toEqual(groups);
+ });
+});
diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts
new file mode 100644
index 0000000000000..e923767c892cd
--- /dev/null
+++ b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { each, isFunction } from 'lodash';
+
+/**
+ * Like _.groupBy, but allows specifying multiple groups for a
+ * single object.
+ *
+ * organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
+ * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]}
+ *
+ * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
+ * // Object {'1,2,3': Array[1], '1,4': Array[1]}
+ *
+ * @param {array} collection - the list of values to organize
+ * @param {Function} callback - either a property name, or a callback.
+ * @return {object}
+ */
+export function organizeBy(collection: object[], callback: ((obj: object) => string) | string) {
+ const buckets: { [key: string]: object[] } = {};
+
+ function add(key: string, obj: object) {
+ if (!buckets[key]) {
+ buckets[key] = [];
+ }
+ buckets[key].push(obj);
+ }
+
+ each(collection, (obj: Record) => {
+ const keys = isFunction(callback) ? callback(obj) : obj[callback];
+
+ if (!Array.isArray(keys)) {
+ add(keys, obj);
+ return;
+ }
+
+ let length = keys.length;
+ while (length-- > 0) {
+ add(keys[length], obj);
+ }
+ });
+
+ return buckets;
+}
diff --git a/src/legacy/ui/public/indexed_array/indexed_array.js b/src/legacy/ui/public/indexed_array/indexed_array.js
index 96b37a1423be0..39c79b2f021a3 100644
--- a/src/legacy/ui/public/indexed_array/indexed_array.js
+++ b/src/legacy/ui/public/indexed_array/indexed_array.js
@@ -19,7 +19,7 @@
import _ from 'lodash';
import { inflector } from './inflector';
-import { organizeBy } from '../utils/collection';
+import { organizeBy } from './helpers/organize_by';
const pathGetter = _(_.get)
.rearg(1, 0)
diff --git a/src/legacy/ui/public/management/_index.scss b/src/legacy/ui/public/management/_index.scss
deleted file mode 100644
index 30ac0c9fe9b27..0000000000000
--- a/src/legacy/ui/public/management/_index.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import './components/index';
\ No newline at end of file
diff --git a/src/legacy/ui/public/management/components/__snapshots__/sidebar_nav.test.ts.snap b/src/legacy/ui/public/management/components/__snapshots__/sidebar_nav.test.ts.snap
deleted file mode 100644
index 3364bee33a544..0000000000000
--- a/src/legacy/ui/public/management/components/__snapshots__/sidebar_nav.test.ts.snap
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Management filters and filters and maps section objects into SidebarNav items 1`] = `
-Array [
- Object {
- "data-test-subj": "activeSection",
- "href": undefined,
- "icon": null,
- "id": "activeSection",
- "isSelected": false,
- "items": Array [
- Object {
- "data-test-subj": "item",
- "href": undefined,
- "icon": null,
- "id": "item",
- "isSelected": false,
- "name": "item",
- },
- ],
- "name": "activeSection",
- },
-]
-`;
diff --git a/src/legacy/ui/public/management/components/sidebar_nav.tsx b/src/legacy/ui/public/management/components/sidebar_nav.tsx
deleted file mode 100644
index cd3d85090dce0..0000000000000
--- a/src/legacy/ui/public/management/components/sidebar_nav.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { EuiIcon, EuiSideNav, IconType, EuiScreenReaderOnly } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-import { IndexedArray } from 'ui/indexed_array';
-
-interface Subsection {
- disabled: boolean;
- visible: boolean;
- id: string;
- display: string;
- url?: string;
- icon?: IconType;
-}
-interface Section extends Subsection {
- visibleItems: IndexedArray;
-}
-
-const sectionVisible = (section: Subsection) => !section.disabled && section.visible;
-const sectionToNav = (selectedId: string) => ({ display, id, url, icon }: Subsection) => ({
- id,
- name: display,
- icon: icon ? : null,
- isSelected: selectedId === id,
- href: url,
- 'data-test-subj': id,
-});
-
-export const sideNavItems = (sections: Section[], selectedId: string) =>
- sections
- .filter(sectionVisible)
- .filter(section => section.visibleItems.filter(sectionVisible).length)
- .map(section => ({
- items: section.visibleItems.filter(sectionVisible).map(sectionToNav(selectedId)),
- ...sectionToNav(selectedId)(section),
- }));
-
-interface SidebarNavProps {
- sections: Section[];
- selectedId: string;
-}
-
-interface SidebarNavState {
- isSideNavOpenOnMobile: boolean;
-}
-
-export class SidebarNav extends React.Component {
- constructor(props: SidebarNavProps) {
- super(props);
- this.state = {
- isSideNavOpenOnMobile: false,
- };
- }
-
- public render() {
- const HEADER_ID = 'management-nav-header';
-
- return (
- <>
-
-