diff --git a/.buildkite/pipelines/pull_request/deploy_cloud.yml b/.buildkite/pipelines/pull_request/deploy_cloud.yml new file mode 100644 index 0000000000000..c6a15ec32e179 --- /dev/null +++ b/.buildkite/pipelines/pull_request/deploy_cloud.yml @@ -0,0 +1,7 @@ +steps: + - command: .buildkite/scripts/steps/cloud/build_and_deploy.sh + label: 'Build and Deploy to Cloud' + agents: + queue: n2-2 + depends_on: build + timeout_in_minutes: 30 diff --git a/.buildkite/pipelines/purge_cloud_deployments.yml b/.buildkite/pipelines/purge_cloud_deployments.yml new file mode 100644 index 0000000000000..8287abf2ca5a2 --- /dev/null +++ b/.buildkite/pipelines/purge_cloud_deployments.yml @@ -0,0 +1,4 @@ +steps: + - command: .buildkite/scripts/steps/cloud/purge.sh + label: Purge old cloud deployments + timeout_in_minutes: 10 diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index d05fe178b72db..a7fbcc0ea4b92 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -13,30 +13,6 @@ else node scripts/build fi -if [[ "${GITHUB_PR_LABELS:-}" == *"ci:deploy-cloud"* ]]; then - echo "--- Build and push Kibana Cloud Distribution" - - echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co - trap 'docker logout docker.elastic.co' EXIT - - node scripts/build \ - --skip-initialize \ - --skip-generic-folders \ - --skip-platform-folders \ - --skip-archives \ - --docker-images \ - --docker-tag-qualifier="$GIT_COMMIT" \ - --docker-push \ - --skip-docker-ubi \ - --skip-docker-ubuntu \ - --skip-docker-contexts - - CLOUD_IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" docker.elastic.co/kibana-ci/kibana-cloud) - cat << EOF | buildkite-agent annotate --style "info" --context cloud-image - Cloud image: $CLOUD_IMAGE -EOF -fi - echo "--- Archive Kibana Distribution" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$KIBANA_DIR/install/kibana" diff --git a/.buildkite/scripts/lifecycle/pre_command.sh b/.buildkite/scripts/lifecycle/pre_command.sh index b957203fc7912..7adae7ff74904 100755 --- a/.buildkite/scripts/lifecycle/pre_command.sh +++ b/.buildkite/scripts/lifecycle/pre_command.sh @@ -92,6 +92,9 @@ export KIBANA_DOCKER_USERNAME KIBANA_DOCKER_PASSWORD="$(retry 5 5 vault read -field=password secret/kibana-issues/dev/container-registry)" export KIBANA_DOCKER_PASSWORD +EC_API_KEY="$(retry 5 5 vault read -field=pr_deploy_api_key secret/kibana-issues/dev/kibana-ci-cloud-deploy)" +export EC_API_KEY + SYNTHETICS_SERVICE_USERNAME="$(retry 5 5 vault read -field=username secret/kibana-issues/dev/kibana-ci-synthetics-credentials)" export SYNTHETICS_SERVICE_USERNAME diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 1df3b5f64b1df..882f76ebeedda 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -103,6 +103,10 @@ const uploadPipeline = (pipelineContent) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/uptime.yml')); } + if (process.env.GITHUB_PR_LABELS.includes('ci:deploy-cloud')) { + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/deploy_cloud.yml')); + } + pipeline.push(getPipeline('.buildkite/pipelines/pull_request/post_build.yml')); uploadPipeline(pipeline.join('\n')); diff --git a/.buildkite/scripts/steps/cloud/build_and_deploy.sh b/.buildkite/scripts/steps/cloud/build_and_deploy.sh new file mode 100755 index 0000000000000..9ea6c4f445328 --- /dev/null +++ b/.buildkite/scripts/steps/cloud/build_and_deploy.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +.buildkite/scripts/bootstrap.sh + +export KBN_NP_PLUGINS_BUILT=true + +VERSION="$(jq -r '.version' package.json)-SNAPSHOT" + +echo "--- Download kibana distribution" + +mkdir -p ./target +buildkite-agent artifact download "kibana-$VERSION-linux-x86_64.tar.gz" ./target --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" + +echo "--- Build and push Kibana Cloud Distribution" + +echo "$KIBANA_DOCKER_PASSWORD" | docker login -u "$KIBANA_DOCKER_USERNAME" --password-stdin docker.elastic.co +trap 'docker logout docker.elastic.co' EXIT + +node scripts/build \ + --skip-initialize \ + --skip-generic-folders \ + --skip-platform-folders \ + --skip-archives \ + --docker-images \ + --docker-tag-qualifier="$GIT_COMMIT" \ + --docker-push \ + --skip-docker-ubi \ + --skip-docker-ubuntu \ + --skip-docker-contexts + +CLOUD_IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" docker.elastic.co/kibana-ci/kibana-cloud) +CLOUD_DEPLOYMENT_NAME="kibana-pr-$BUILDKITE_PULL_REQUEST" + +jq ' + .resources.kibana[0].plan.kibana.docker_image = "'$CLOUD_IMAGE'" | + .name = "'$CLOUD_DEPLOYMENT_NAME'" | + .resources.kibana[0].plan.kibana.version = "'$VERSION'" | + .resources.elasticsearch[0].plan.elasticsearch.version = "'$VERSION'" + ' .buildkite/scripts/steps/cloud/deploy.json > /tmp/deploy.json + +CLOUD_DEPLOYMENT_ID=$(ecctl deployment list --output json | jq -r '.deployments[] | select(.name == "'$CLOUD_DEPLOYMENT_NAME'") | .id') +JSON_FILE=$(mktemp --suffix ".json") +if [ -z "${CLOUD_DEPLOYMENT_ID}" ]; then + ecctl deployment create --track --output json --file /tmp/deploy.json &> "$JSON_FILE" + CLOUD_DEPLOYMENT_USERNAME=$(jq --slurp '.[]|select(.resources).resources[] | select(.credentials).credentials.username' "$JSON_FILE") + CLOUD_DEPLOYMENT_PASSWORD=$(jq --slurp '.[]|select(.resources).resources[] | select(.credentials).credentials.password' "$JSON_FILE") + CLOUD_DEPLOYMENT_ID=$(jq -r --slurp '.[0].id' "$JSON_FILE") + CLOUD_DEPLOYMENT_STATUS_MESSAGES=$(jq --slurp '[.[]|select(.resources == null)]' "$JSON_FILE") + + # Refresh vault token + VAULT_ROLE_ID="$(retry 5 15 gcloud secrets versions access latest --secret=kibana-buildkite-vault-role-id)" + VAULT_SECRET_ID="$(retry 5 15 gcloud secrets versions access latest --secret=kibana-buildkite-vault-secret-id)" + VAULT_TOKEN=$(retry 5 30 vault write -field=token auth/approle/login role_id="$VAULT_ROLE_ID" secret_id="$VAULT_SECRET_ID") + retry 5 30 vault login -no-print "$VAULT_TOKEN" + + retry 5 5 vault write "secret/kibana-issues/dev/cloud-deploy/$CLOUD_DEPLOYMENT_NAME" username="$CLOUD_DEPLOYMENT_USERNAME" password="$CLOUD_DEPLOYMENT_PASSWORD" +else + ecctl deployment update "$CLOUD_DEPLOYMENT_ID" --track --output json --file /tmp/deploy.json &> "$JSON_FILE" +fi + +CLOUD_DEPLOYMENT_KIBANA_URL=$(ecctl deployment show "$CLOUD_DEPLOYMENT_ID" | jq -r '.resources.kibana[0].info.metadata.aliased_url') +CLOUD_DEPLOYMENT_ELASTICSEARCH_URL=$(ecctl deployment show "$CLOUD_DEPLOYMENT_ID" | jq -r '.resources.elasticsearch[0].info.metadata.aliased_url') + +cat << EOF | buildkite-agent annotate --style "info" --context cloud + ### Cloud Deployment + + Kibana: $CLOUD_DEPLOYMENT_KIBANA_URL + + Elasticsearch: $CLOUD_DEPLOYMENT_ELASTICSEARCH_URL + + Credentials: \`vault read secret/kibana-issues/dev/cloud-deploy/$CLOUD_DEPLOYMENT_NAME\` + + Image: $CLOUD_IMAGE +EOF + +buildkite-agent meta-data set pr_comment:deploy_cloud:head "* [Cloud Deployment](${CLOUD_DEPLOYMENT_KIBANA_URL})" diff --git a/.buildkite/scripts/steps/cloud/deploy.json b/.buildkite/scripts/steps/cloud/deploy.json new file mode 100644 index 0000000000000..37768144f138a --- /dev/null +++ b/.buildkite/scripts/steps/cloud/deploy.json @@ -0,0 +1,163 @@ +{ + "resources": { + "elasticsearch": [ + { + "region": "gcp-us-west2", + "settings": { + "dedicated_masters_threshold": 6 + }, + "plan": { + "autoscaling_enabled": false, + "cluster_topology": [ + { + "zone_count": 2, + "instance_configuration_id": "gcp.coordinating.1", + "node_roles": ["ingest", "remote_cluster_client"], + "id": "coordinating", + "size": { + "resource": "memory", + "value": 0 + }, + "elasticsearch": { + "enabled_built_in_plugins": [] + } + }, + { + "zone_count": 1, + "elasticsearch": { + "node_attributes": { + "data": "hot" + }, + "enabled_built_in_plugins": [] + }, + "instance_configuration_id": "gcp.data.highio.1", + "node_roles": [ + "master", + "ingest", + "transform", + "data_hot", + "remote_cluster_client", + "data_content" + ], + "id": "hot_content", + "size": { + "value": 1024, + "resource": "memory" + } + }, + { + "zone_count": 2, + "elasticsearch": { + "node_attributes": { + "data": "warm" + }, + "enabled_built_in_plugins": [] + }, + "instance_configuration_id": "gcp.data.highstorage.1", + "node_roles": ["data_warm", "remote_cluster_client"], + "id": "warm", + "size": { + "resource": "memory", + "value": 0 + } + }, + { + "zone_count": 1, + "elasticsearch": { + "node_attributes": { + "data": "cold" + }, + "enabled_built_in_plugins": [] + }, + "instance_configuration_id": "gcp.data.highstorage.1", + "node_roles": ["data_cold", "remote_cluster_client"], + "id": "cold", + "size": { + "resource": "memory", + "value": 0 + } + }, + { + "zone_count": 1, + "elasticsearch": { + "node_attributes": { + "data": "frozen" + }, + "enabled_built_in_plugins": [] + }, + "instance_configuration_id": "gcp.es.datafrozen.n1.64x10x95", + "node_roles": ["data_frozen"], + "id": "frozen", + "size": { + "resource": "memory", + "value": 0 + } + }, + { + "zone_count": 3, + "instance_configuration_id": "gcp.master.1", + "node_roles": ["master", "remote_cluster_client"], + "id": "master", + "size": { + "resource": "memory", + "value": 0 + }, + "elasticsearch": { + "enabled_built_in_plugins": [] + } + }, + { + "zone_count": 1, + "instance_configuration_id": "gcp.ml.1", + "node_roles": ["ml", "remote_cluster_client"], + "id": "ml", + "size": { + "resource": "memory", + "value": 0 + }, + "elasticsearch": { + "enabled_built_in_plugins": [] + } + } + ], + "elasticsearch": { + "version": null + }, + "deployment_template": { + "id": "gcp-io-optimized-v2" + } + }, + "ref_id": "main-elasticsearch" + } + ], + "enterprise_search": [], + "kibana": [ + { + "elasticsearch_cluster_ref_id": "main-elasticsearch", + "region": "gcp-us-west2", + "plan": { + "cluster_topology": [ + { + "instance_configuration_id": "gcp.kibana.1", + "zone_count": 1, + "size": { + "resource": "memory", + "value": 1024 + } + } + ], + "kibana": { + "version": null, + "docker_image": null + } + }, + "ref_id": "main-kibana" + } + ], + "apm": [] + }, + "name": null, + "metadata": { + "system_owned": false + } +} diff --git a/.buildkite/scripts/steps/cloud/purge.js b/.buildkite/scripts/steps/cloud/purge.js new file mode 100644 index 0000000000000..0eccb55cef830 --- /dev/null +++ b/.buildkite/scripts/steps/cloud/purge.js @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { execSync } = require('child_process'); + +const deploymentsListJson = execSync('ecctl deployment list --output json').toString(); +const { deployments } = JSON.parse(deploymentsListJson); + +const prDeployments = deployments.filter((deployment) => deployment.name.startsWith('kibana-pr-')); + +const deploymentsToPurge = []; + +const NOW = new Date().getTime() / 1000; + +for (const deployment of prDeployments) { + try { + const prNumber = deployment.name.match(/^kibana-pr-([0-9]+)$/)[1]; + const prJson = execSync(`gh pr view '${prNumber}' --json state,labels,commits`).toString(); + const pullRequest = JSON.parse(prJson); + + const lastCommit = pullRequest.commits.slice(-1)[0]; + const lastCommitTimestamp = new Date(lastCommit.committedDate).getTime() / 1000; + + if (pullRequest.state !== 'open') { + console.log(`Pull Request #${prNumber} is no longer open, will delete associated deployment`); + deploymentsToPurge.push(deployment); + } else if (!pullRequest.labels.filter((label) => label.name === 'ci:deploy-cloud')) { + console.log( + `Pull Request #${prNumber} no longer has the ci:deploy-cloud label, will delete associated deployment` + ); + deploymentsToPurge.push(deployment); + } else if (lastCommitTimestamp < NOW - 60 * 60 * 24 * 7) { + console.log( + `Pull Request #${prNumber} has not been updated in more than 7 days, will delete associated deployment` + ); + deploymentsToPurge.push(deployment); + } + } catch (ex) { + console.error(ex.toString()); + // deploymentsToPurge.push(deployment); // TODO should we delete on error? + } +} + +for (const deployment of deploymentsToPurge) { + console.log(`Scheduling deployment for deletion: ${deployment.name} / ${deployment.id}`); + try { + execSync(`ecctl deployment shutdown --force '${deployment.id}'`, { stdio: 'inherit' }); + } catch (ex) { + console.error(ex.toString()); + } +} diff --git a/.buildkite/scripts/steps/cloud/purge.sh b/.buildkite/scripts/steps/cloud/purge.sh new file mode 100755 index 0000000000000..265148c013400 --- /dev/null +++ b/.buildkite/scripts/steps/cloud/purge.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +node .buildkite/scripts/steps/cloud/purge.js diff --git a/docs/management/connectors/action-types/index.asciidoc b/docs/management/connectors/action-types/index.asciidoc index 98f7dac4de81d..01e9e3b22e2c2 100644 --- a/docs/management/connectors/action-types/index.asciidoc +++ b/docs/management/connectors/action-types/index.asciidoc @@ -105,7 +105,7 @@ experimental[] {kib} offers a preconfigured index connector to facilitate indexi [WARNING] ================================================== -This functionality is experimental and may be changed or removed completely in a future release. +This functionality is in technical preview and may be changed or removed completely in a future release. ================================================== To use this connector, set the <> configuration to `true`. diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc index dbff211cef372..ccd3f41b9d886 100644 --- a/docs/management/upgrade-assistant/index.asciidoc +++ b/docs/management/upgrade-assistant/index.asciidoc @@ -11,10 +11,13 @@ enables you to see if you are using deprecated features, and guides you through the process of resolving issues. If you have indices that were created prior to 7.0, -you can use the assistant to reindex them so they can be accessed from 8.0. +you can use the assistant to reindex them so they can be accessed from 8.0+. IMPORTANT: To see the most up-to-date deprecation information before -upgrading to 8.0, upgrade to the latest 7.n release. +upgrading to 8.0, upgrade to the latest {prev-major-last} release. + +For more information about upgrading, +refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] [discrete] === Required permissions diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4e0f0d4f99e66..ff6ccbd6fab36 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -385,3 +385,13 @@ This content has moved. Refer to <>. == Kibana role management. This content has moved. Refer to <>. + +[role="exclude" logging-configuration-changes] +== Logging configuration changes + +This content has moved. Refer to <>. + +[role="exclude" upgrade-migrations] +== Upgrade migrations + +This content has moved. Refer to <>. diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index 286bb71542b3a..b7423d7c37b31 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -33,7 +33,7 @@ This flag will enable automatic warn and error logging if task manager self dete The amount of seconds we allow a task to delay before printing a warning server log. Defaults to 60. `xpack.task_manager.ephemeral_tasks.enabled`:: -Enables an experimental feature that executes a limited (and configurable) number of actions in the same task as the alert which triggered them. +Enables a technical preview feature that executes a limited (and configurable) number of actions in the same task as the alert which triggered them. These action tasks will reduce the latency of the time it takes an action to run after it's triggered, but are not persisted as SavedObjects. These non-persisted action tasks have a risk that they won't be run at all if the Kibana instance running them exits unexpectedly. Defaults to false. diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index c828b837d8efd..d8e08b460e5f6 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -1,114 +1,34 @@ [[upgrade]] == Upgrade {kib} -You can always upgrade to the latest patch release or from one minor version -to another within the same major version series. +To upgrade from 7.16 or earlier to {version}, +**You must first upgrade to {prev-major-last}**. +This enables you to use the Upgrade Assistant to +{stack-ref}/upgrading-elastic-stack.html#prepare-to-upgrade[prepare to upgrade]. +You must resolve all critical issues identified by the Upgrade Assistant +before proceeding with the upgrade. -For major version upgrades: - -. Upgrade to the last minor version released before the new major version. -. Use the Upgrade Assistant to determine what changes you need to make before the major version upgrade. -. When you've addressed all the critical issues, upgrade {es} and then upgrade {kib}. - -IMPORTANT: You can upgrade to pre-release versions of 8.0 for testing, -but upgrading from a pre-release to the final GA version is not supported. -Pre-releases should only be used for testing in a temporary environment. - -[discrete] -[[upgrade-paths]] -=== Recommended upgrade paths to 8.0 - -[cols="<1,3",options="header",] -|==== -|Upgrading from -|Upgrade path - -|7.16 -|Upgrade to 8.0 - -|6.8–7.15 -a| - -. Upgrade to 7.16 -. Upgrade to 8.0 - -|6.0–6.7 -a| - -. Upgrade to 6.8 -. Upgrade to 7.16 -. Upgrade to 8.0 -|==== - -[float] -[[upgrade-before-you-begin]] -=== Before you begin +{kib} does not support rolling upgrades. +You must shut down all {kib} instances, install the new software, and restart {kib}. +Upgrading while older {kib} instances are running can cause data loss or upgrade failures. [WARNING] ==== -{kib} automatically runs upgrade migrations when required. To roll back to an -earlier version in case of an upgrade failure, you **must** have a +{kib} automatically runs <> +when required. +In case of an upgrade failure, you can roll back to an +earlier version of {kib}. To roll back, you **must** have a {ref}/snapshot-restore.html[backup snapshot] that includes the `kibana` feature state. Snapshots include this feature state by default. - -For more information, refer to <>. ==== -Before you upgrade {kib}: +For more information about upgrading, +refer to {stack-ref}/upgrading-elastic-stack.html[Upgrading to Elastic {version}.] -* Consult the <>. -* {ref}/snapshots-take-snapshot.html[Take a snapshot] of your data. To roll back to an earlier version, the snapshot must include the `kibana` feature state. -* Before you upgrade production servers, test the upgrades in a dev environment. -* See <> for common reasons upgrades fail and how to prevent these. -* If you are using custom plugins, check that a compatible version is - available. -* Shut down all {kib} instances. Running more than one {kib} version against - the same Elasticseach index is unsupported. Upgrading while older {kib} - instances are running can cause data loss or upgrade failures. - -NOTE: {kib} logging system may have changed, depending on your target version. For details, see <>. - -To identify the changes you need to make to upgrade, and to enable you to -perform an Elasticsearch rolling upgrade with no downtime, you must upgrade to -6.7 before you upgrade to 7.0. - -For a comprehensive overview of the upgrade process, refer to -*{stack-ref}/upgrading-elastic-stack.html[Upgrading the Elastic Stack]*. - -[float] -[[upgrade-5x-earlier]] -=== Upgrade from 5.x or earlier -{es} can read indices created in the previous major version. Before you upgrade -to 7.0.0, you must reindex or delete any indices created in 5.x or earlier. -For more information, refer to -{stack-ref}/upgrading-elastic-stack.html[Upgrading the Elastic Stack]. - -When your reindex is complete, follow the <> -instructions. - -[float] -[[upgrade-6x]] -=== Upgrade from 6.x - -The recommended path is to upgrade to 6.8 before upgrading to 7.0. This makes it -easier to identify the required changes, and enables you to use the Upgrade -Assistant to prepare for your upgrade to 7.0. - -TIP: The ability to import {kib} 6.x saved searches, visualizations, and -dashboards is supported. - -[float] -[[upgrade-67]] -=== Upgrade from 6.8 -To help you prepare for your upgrade to 7.0, 6.8 includes an https://www.elastic.co/guide/en/kibana/6.8/upgrade-assistant.html[Upgrade Assistant] -To access the assistant, go to *Management > 7.0 Upgrade Assistant*. - -After you have addressed any issues that were identified by the Upgrade -Assistant, <>. - - -include::upgrade/upgrade-standard.asciidoc[] +IMPORTANT: You can upgrade to pre-release versions for testing, +but upgrading from a pre-release to the General Available version is not supported. +Pre-releases should only be used for testing in a temporary environment. -include::upgrade/upgrade-migrations.asciidoc[] +include::upgrade/upgrade-migrations.asciidoc[leveloffset=-1] include::upgrade/logging-configuration-changes.asciidoc[] diff --git a/docs/setup/upgrade/logging-configuration-changes.asciidoc b/docs/setup/upgrade/logging-configuration-changes.asciidoc index a7a86fcb45b14..4d5f5f732536e 100644 --- a/docs/setup/upgrade/logging-configuration-changes.asciidoc +++ b/docs/setup/upgrade/logging-configuration-changes.asciidoc @@ -1,4 +1,5 @@ -[[logging-configuration-changes]] +[discrete] +[[logging-config-changes]] === Logging configuration changes WARNING: {kib} 8.0 and later uses a new logging system. Be sure to read the documentation for your version of {kib} before proceeding. diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index e9e1b757fd71d..fc921f9118bdf 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -1,5 +1,6 @@ -[[upgrade-migrations]] -=== Upgrade migrations +[float] +[[saved-object-migrations]] +=== Saved object migrations Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version. diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index 15d5db7395ec6..58f61b79f3ba6 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -462,7 +462,7 @@ When "thom" logs in, a "user_login" {kib} audit event is written: [source,json] ------------- -{"event":{"action":"user_login","category":["authentication"],"outcome":"success"},"user":{"name":"thom","roles":["superuser"]},"@timestamp":"2022-01-25T09:40:39.267-05:00","message":"User [thom] has logged in using basic provider [name=basic]","trace":{"id":"818cbf3..."}} +{"event":{"action":"user_login","category":["authentication"],"outcome":"success"},"kibana":{"session_id":"ab93zdA..."},"user":{"name":"thom","roles":["superuser"]},"@timestamp":"2022-01-25T09:40:39.267-05:00","message":"User [thom] has logged in using basic provider [name=basic]","trace":{"id":"818cbf3..."}} ------------- The `trace.id` value `"818cbf3..."` in the {kib} audit event can be correlated with the `opaque_id` value in these six {es} audit events: diff --git a/packages/elastic-apm-synthtrace/README.md b/packages/elastic-apm-synthtrace/README.md index cdbd536831676..bf497fecd81c8 100644 --- a/packages/elastic-apm-synthtrace/README.md +++ b/packages/elastic-apm-synthtrace/README.md @@ -1,6 +1,6 @@ # @elastic/apm-synthtrace -`@elastic/apm-synthtrace` is an experimental tool to generate synthetic APM data. It is intended to be used for development and testing of the Elastic APM app in Kibana. +`@elastic/apm-synthtrace` is a tool in technical preview to generate synthetic APM data. It is intended to be used for development and testing of the Elastic APM app in Kibana. At a high-level, the module works by modeling APM events/metricsets with [a fluent API](https://en.wikipedia.org/wiki/Fluent_interface). The models can then be serialized and converted to Elasticsearch documents. In the future we might support APM Server as an output as well. @@ -98,19 +98,20 @@ Via the CLI, you can upload scenarios, either using a fixed time range or contin For a fixed time window: `$ node packages/elastic-apm-synthtrace/src/scripts/run packages/elastic-apm-synthtrace/src/scripts/examples/01_simple_trace.ts --target=http://admin:changeme@localhost:9200 --from=now-24h --to=now` -The script will try to automatically find bootstrapped APM indices. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ +The script will try to automatically find bootstrapped APM indices. **If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.** The following options are supported: -| Option | Description | Default | -| ------------------| ------------------------------------------------------- | ------------ | -| `--target` | Elasticsearch target, including username/password. | **Required** | -| `--from` | The start of the time window. | `now - 15m` | -| `--to` | The end of the time window. | `now` | -| `--live` | Continously ingest data | `false` | -| `--clean` | Clean APM indices before indexing new data. | `false` | -| `--workers` | Amount of Node.js worker threads | `5` | -| `--bucketSize` | Size of bucket for which to generate data. | `15m` | -| `--interval` | The interval at which to index data. | `10s` | -| `--clientWorkers` | Number of simultaneously connected ES clients | `5` | -| `--batchSize` | Number of documents per bulk index request | `1000` | -| `--logLevel` | Log level. | `info` | + +| Option | Description | Default | +| ----------------- | -------------------------------------------------- | ------------ | +| `--target` | Elasticsearch target, including username/password. | **Required** | +| `--from` | The start of the time window. | `now - 15m` | +| `--to` | The end of the time window. | `now` | +| `--live` | Continously ingest data | `false` | +| `--clean` | Clean APM indices before indexing new data. | `false` | +| `--workers` | Amount of Node.js worker threads | `5` | +| `--bucketSize` | Size of bucket for which to generate data. | `15m` | +| `--interval` | The interval at which to index data. | `10s` | +| `--clientWorkers` | Number of simultaneously connected ES clients | `5` | +| `--batchSize` | Number of documents per bulk index request | `1000` | +| `--logLevel` | Log level. | `info` | diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index b2e8641cd62f2..5f4c07ee6067c 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -64,6 +64,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { }, discover: { guide: `${KIBANA_DOCS}discover.html`, + fieldStatistics: `${KIBANA_DOCS}show-field-statistics.html`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, @@ -234,6 +235,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`, + frozenIndices: `${ELASTICSEARCH_DOCS}frozen-indices.html`, hiddenIndices: `${ELASTICSEARCH_DOCS}multi-index.html#hidden`, ilm: `${ELASTICSEARCH_DOCS}index-lifecycle-management.html`, ilmForceMerge: `${ELASTICSEARCH_DOCS}ilm-forcemerge.html`, diff --git a/src/plugins/advanced_settings/public/management_app/components/call_outs/__snapshots__/call_outs.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/call_outs/__snapshots__/call_outs.test.tsx.snap index b3863b5452d90..7da96ad98f1bf 100644 --- a/src/plugins/advanced_settings/public/management_app/components/call_outs/__snapshots__/call_outs.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/call_outs/__snapshots__/call_outs.test.tsx.snap @@ -15,7 +15,7 @@ exports[`CallOuts should render normally 1`] = ` >

diff --git a/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx b/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx index b4eea59249c63..dabf44e2ba948 100644 --- a/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/call_outs/call_outs.tsx @@ -29,7 +29,7 @@ export const CallOuts = () => { id="advancedSettings.callOutCautionDescription" defaultMessage="Be careful in here, these settings are for very advanced users only. Tweaks you make here can break large portions of Kibana. - Some of these settings may be undocumented, unsupported or experimental. + Some of these settings may be undocumented, unsupported or in technical preview. If a field has a default value, blanking the field will reset it to its default which may be unacceptable given other configuration directives. Deleting a custom setting will permanently remove it from Kibana's config." diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx index 105bd339cbca4..92882bc58f833 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx @@ -168,6 +168,29 @@ describe('HeatmapComponent', function () { }); }); + it('computes the bands correctly if only value accessor is provided', async () => { + const newData: Datatable = { + type: 'datatable', + rows: [{ 'col-0-1': 571.806 }], + columns: [{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } }], + }; + const newProps = { + ...wrapperProps, + data: newData, + args: { ...wrapperProps.args, lastRangeIsRightOpen: false }, + } as unknown as HeatmapRenderProps; + const component = mountWithIntl(); + await act(async () => { + expect(component.find(Heatmap).prop('colorScale')).toEqual({ + bands: [ + { color: 'rgb(0, 0, 0)', end: 0, start: 0 }, + { color: 'rgb(112, 38, 231)', end: Infinity, start: 571.806 }, + ], + type: 'bands', + }); + }); + }); + it('renders the axis titles', () => { const component = shallowWithIntl(); expect(component.find(Heatmap).prop('xAxisTitle')).toEqual('Dest'); diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index 00e777cc5156c..0f3cc5d05de4d 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -195,122 +195,8 @@ export const HeatmapComponent: FC = memo( const xAxisColumn = table.columns[xAxisColumnIndex]; const yAxisColumn = table.columns[yAxisColumnIndex]; const valueColumn = table.columns.find((v) => v.id === valueAccessor); - - if (!valueColumn) { - // Chart is not ready - return null; - } - - let chartData = table.rows.filter((v) => typeof v[valueAccessor!] === 'number'); - if (!chartData || !chartData.length) { - return ; - } - - if (!yAxisColumn) { - // required for tooltip - chartData = chartData.map((row) => { - return { - ...row, - unifiedY: '', - }; - }); - } - const { min, max } = minMaxByColumnId[valueAccessor!]; - // formatters const xAxisMeta = xAxisColumn?.meta; - const xValuesFormatter = formatFactory(xAxisMeta?.params); - const metricFormatter = formatFactory( - typeof args.valueAccessor === 'string' ? valueColumn.meta.params : args?.valueAccessor?.format - ); const isTimeBasedSwimLane = xAxisMeta?.type === 'date'; - const dateHistogramMeta = xAxisColumn - ? search.aggs.getDateHistogramMetaDataByDatatableColumn(xAxisColumn) - : undefined; - - // Fallback to the ordinal scale type when a single row of data is provided. - // Related issue https://github.com/elastic/elastic-charts/issues/1184 - let xScale: HeatmapSpec['xScale'] = { type: ScaleType.Ordinal }; - if (isTimeBasedSwimLane && chartData.length > 1) { - const dateInterval = dateHistogramMeta?.interval; - const esInterval = dateInterval ? search.aggs.parseEsInterval(dateInterval) : undefined; - if (esInterval) { - xScale = { - type: ScaleType.Time, - interval: - esInterval.type === 'fixed' - ? { - type: 'fixed', - unit: esInterval.unit as ESFixedIntervalUnit, - value: esInterval.value, - } - : { - type: 'calendar', - unit: esInterval.unit as ESCalendarIntervalUnit, - value: esInterval.value, - }, - }; - } - } - - const tooltip: TooltipProps = { - type: args.showTooltip ? TooltipType.Follow : TooltipType.None, - }; - - const valueFormatter = (d: number) => { - let value = d; - - if (args.percentageMode) { - const percentageNumber = (Math.abs(value - min) / (max - min)) * 100; - value = parseInt(percentageNumber.toString(), 10) / 100; - } - return `${metricFormatter.convert(value) ?? ''}`; - }; - - const { colors, ranges } = computeColorRanges( - paletteService, - paletteParams, - isDarkTheme ? '#000' : '#fff', - minMaxByColumnId[valueAccessor!] - ); - - // adds a very small number to the max value to make sure the max value will be included - const smattering = 0.00001; - let endValue = max + smattering; - if (paletteParams?.rangeMax || paletteParams?.rangeMax === 0) { - endValue = - (paletteParams?.range === 'number' - ? paletteParams.rangeMax - : min + ((max - min) * paletteParams.rangeMax) / 100) + smattering; - } - - const overwriteColors = uiState?.get('vis.colors') ?? null; - - const bands = ranges.map((start, index, array) => { - // by default the last range is right-open - let end = index === array.length - 1 ? Number.POSITIVE_INFINITY : array[index + 1]; - // if the lastRangeIsRightOpen is set to false, we need to set the last range to the max value - if (args.lastRangeIsRightOpen === false) { - const lastBand = max === start ? Number.POSITIVE_INFINITY : endValue; - end = index === array.length - 1 ? lastBand : array[index + 1]; - } - - let overwriteArrayIdx; - - if (end === Number.POSITIVE_INFINITY) { - overwriteArrayIdx = `≥ ${start}`; - } else { - overwriteArrayIdx = `${metricFormatter.convert(start)} - ${metricFormatter.convert(end)}`; - } - - const overwriteColor = overwriteColors?.[overwriteArrayIdx]; - return { - // with the default continuity:above the every range is left-closed - start, - end, - // the current colors array contains a duplicated color at the beginning that we need to skip - color: overwriteColor ?? colors[index + 1], - }; - }); const onElementClick = useCallback( (e: HeatmapElementEvent[]) => { @@ -421,6 +307,125 @@ export const HeatmapComponent: FC = memo( ] ); + if (!valueColumn) { + // Chart is not ready + return null; + } + + let chartData = table.rows.filter((v) => typeof v[valueAccessor!] === 'number'); + if (!chartData || !chartData.length) { + return ; + } + + if (!yAxisColumn) { + // required for tooltip + chartData = chartData.map((row) => { + return { + ...row, + unifiedY: '', + }; + }); + } + const { min, max } = minMaxByColumnId[valueAccessor!]; + // formatters + const xValuesFormatter = formatFactory(xAxisMeta?.params); + const metricFormatter = formatFactory( + typeof args.valueAccessor === 'string' ? valueColumn.meta.params : args?.valueAccessor?.format + ); + const dateHistogramMeta = xAxisColumn + ? search.aggs.getDateHistogramMetaDataByDatatableColumn(xAxisColumn) + : undefined; + + // Fallback to the ordinal scale type when a single row of data is provided. + // Related issue https://github.com/elastic/elastic-charts/issues/1184 + let xScale: HeatmapSpec['xScale'] = { type: ScaleType.Ordinal }; + if (isTimeBasedSwimLane && chartData.length > 1) { + const dateInterval = dateHistogramMeta?.interval; + const esInterval = dateInterval ? search.aggs.parseEsInterval(dateInterval) : undefined; + if (esInterval) { + xScale = { + type: ScaleType.Time, + interval: + esInterval.type === 'fixed' + ? { + type: 'fixed', + unit: esInterval.unit as ESFixedIntervalUnit, + value: esInterval.value, + } + : { + type: 'calendar', + unit: esInterval.unit as ESCalendarIntervalUnit, + value: esInterval.value, + }, + }; + } + } + + const tooltip: TooltipProps = { + type: args.showTooltip ? TooltipType.Follow : TooltipType.None, + }; + + const valueFormatter = (d: number) => { + let value = d; + + if (args.percentageMode) { + const percentageNumber = (Math.abs(value - min) / (max - min)) * 100; + value = parseInt(percentageNumber.toString(), 10) / 100; + } + return `${metricFormatter.convert(value) ?? ''}`; + }; + + const { colors, ranges } = computeColorRanges( + paletteService, + paletteParams, + isDarkTheme ? '#000' : '#fff', + minMaxByColumnId[valueAccessor!] + ); + + // adds a very small number to the max value to make sure the max value will be included + const smattering = 0.00001; + let endValueDistinctBounds = max + smattering; + if (paletteParams?.rangeMax || paletteParams?.rangeMax === 0) { + endValueDistinctBounds = + (paletteParams?.range === 'number' + ? paletteParams.rangeMax + : min + ((max - min) * paletteParams.rangeMax) / 100) + smattering; + } + + const overwriteColors = uiState?.get('vis.colors') ?? null; + const hasSingleValue = max === min; + const bands = ranges.map((start, index, array) => { + const isPenultimate = index === array.length - 1; + const nextValue = array[index + 1]; + // by default the last range is right-open + let endValue = isPenultimate ? Number.POSITIVE_INFINITY : nextValue; + const startValue = isPenultimate && hasSingleValue ? min : start; + // if the lastRangeIsRightOpen is set to false, we need to set the last range to the max value + if (args.lastRangeIsRightOpen === false) { + const lastBand = hasSingleValue ? Number.POSITIVE_INFINITY : endValueDistinctBounds; + endValue = isPenultimate ? lastBand : nextValue; + } + + let overwriteArrayIdx; + + if (endValue === Number.POSITIVE_INFINITY) { + overwriteArrayIdx = `≥ ${metricFormatter.convert(startValue)}`; + } else { + overwriteArrayIdx = `${metricFormatter.convert(start)} - ${metricFormatter.convert( + endValue + )}`; + } + + const overwriteColor = overwriteColors?.[overwriteArrayIdx]; + return { + // with the default continuity:above the every range is left-closed + start: startValue, + end: endValue, + // the current colors array contains a duplicated color at the beginning that we need to skip + color: overwriteColor ?? colors[index + 1], + }; + }); + const themeOverrides: PartialTheme = { legend: { labelOptions: { diff --git a/src/plugins/dashboard/server/ui_settings.ts b/src/plugins/dashboard/server/ui_settings.ts index 99eb29a27deaa..6efd68da25f2d 100644 --- a/src/plugins/dashboard/server/ui_settings.ts +++ b/src/plugins/dashboard/server/ui_settings.ts @@ -22,7 +22,7 @@ export const getUISettings = (): Record> => ({ }), description: i18n.translate('dashboard.labs.enableLabsDescription', { defaultMessage: - 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable experimental features in Dashboard.', + 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable technical preview features in Dashboard.', }), value: false, type: 'boolean', diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 51dd883211620..ea6a7bc3446e3 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -94,7 +94,7 @@ export class DataServerPlugin this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); - core.uiSettings.register(getUiSettings()); + core.uiSettings.register(getUiSettings(core.docLinks)); const searchSetup = this.searchService.setup(core, { bfetch, diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 19ecde71e6a19..4783223e49869 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { UiSettingsParams } from 'kibana/server'; +import type { DocLinksServiceSetup, UiSettingsParams } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../common'; const luceneQueryLanguageLabel = i18n.translate('data.advancedSettings.searchQueryLanguageLucene', { @@ -31,7 +31,9 @@ const requestPreferenceOptionLabels = { }), }; -export function getUiSettings(): Record> { +export function getUiSettings( + docLinks: DocLinksServiceSetup +): Record> { return { [UI_SETTINGS.META_FIELDS]: { name: i18n.translate('data.advancedSettings.metaFieldsTitle', { @@ -71,7 +73,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.query.queryStringOptionsText', values: { optionsLink: - '' + + `` + i18n.translate('data.advancedSettings.query.queryStringOptions.optionsLinkText', { defaultMessage: 'Options', }) + @@ -150,7 +152,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.sortOptionsText', values: { optionsLink: - '' + + `` + i18n.translate('data.advancedSettings.sortOptions.optionsLinkText', { defaultMessage: 'Options', }) + @@ -232,7 +234,7 @@ export function getUiSettings(): Record> { setRequestReferenceSetting: `${UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE}`, customSettingValue: '"custom"', requestPreferenceLink: - '' + + `` + i18n.translate( 'data.advancedSettings.courier.customRequestPreference.requestPreferenceLinkText', { @@ -256,7 +258,7 @@ export function getUiSettings(): Record> { 'Controls the {maxRequestsLink} setting used for _msearch requests sent by Kibana. ' + 'Set to 0 to disable this config and use the Elasticsearch default.', values: { - maxRequestsLink: `max_concurrent_shard_requests`, }, }), @@ -265,7 +267,7 @@ export function getUiSettings(): Record> { }, [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: { name: 'Search in frozen indices', - description: `Will include frozen indices in results if enabled. Searching through frozen indices might increase the search time.`, value: false, @@ -444,7 +446,7 @@ export function getUiSettings(): Record> { 'data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', values: { acceptedFormatsLink: - `` + i18n.translate('data.advancedSettings.timepicker.quickRanges.acceptedFormatsLinkText', { defaultMessage: 'accepted formats', @@ -495,7 +497,7 @@ export function getUiSettings(): Record> { 'Elasticsearch terms aggregation. {learnMoreLink}', values: { learnMoreLink: - '' + + `` + i18n.translate('data.advancedSettings.autocompleteValueSuggestionMethodLink', { defaultMessage: 'Learn more.', }) + @@ -517,7 +519,7 @@ export function getUiSettings(): Record> { 'Disable this property to get autocomplete suggestions from your full dataset, rather than from the current time range. {learnMoreLink}', values: { learnMoreLink: - '' + + `` + i18n.translate('data.advancedSettings.autocompleteValueSuggestionMethodLearnMoreLink', { defaultMessage: 'Learn more.', }) + diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index 27cb3cec8be41..879b75986365b 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -14,7 +14,7 @@ import { searchSavedObjectType } from './saved_objects'; export class DiscoverServerPlugin implements Plugin { public setup(core: CoreSetup) { core.capabilities.registerProvider(capabilitiesProvider); - core.uiSettings.register(getUiSettings()); + core.uiSettings.register(getUiSettings(core.docLinks)); core.savedObjects.registerType(searchSavedObjectType); return {}; diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index f35212ba43618..c9c9692e6986b 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { UiSettingsParams } from 'kibana/server'; +import type { DocLinksServiceSetup, UiSettingsParams } from 'kibana/server'; import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, @@ -31,7 +31,9 @@ import { ROW_HEIGHT_OPTION, } from '../common'; -export const getUiSettings: () => Record = () => ({ +export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record = ( + docLinks: DocLinksServiceSetup +) => ({ [DEFAULT_COLUMNS_SETTING]: { name: i18n.translate('discover.advancedSettings.defaultColumnsTitle', { defaultMessage: 'Default columns', @@ -215,7 +217,7 @@ export const getUiSettings: () => Record = () => ({ defaultMessage: `Enable the {fieldStatisticsDocs} to show details such as the minimum and maximum values of a numeric field or a map of a geo field. This functionality is in beta and is subject to change.`, values: { fieldStatisticsDocs: - `` + i18n.translate('discover.advancedSettings.discover.fieldStatisticsLinkText', { defaultMessage: 'Field statistics view', @@ -240,7 +242,7 @@ export const getUiSettings: () => Record = () => ({ defaultMessage: `Controls whether {multiFields} display in the expanded document view. In most cases, multi-fields are the same as the original field. This option is only available when \`searchFieldsFromSource\` is off.`, values: { multiFields: - `` + i18n.translate('discover.advancedSettings.discover.multiFieldsLinkText', { defaultMessage: 'multi-fields', diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx index ee8f15f421487..c7fafcc89f060 100644 --- a/src/plugins/presentation_util/public/i18n/labs.tsx +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -89,7 +89,7 @@ export const LabsStrings = { }), getDescriptionMessage: () => i18n.translate('presentationUtil.labs.components.descriptionMessage', { - defaultMessage: 'Try out our features that are in progress or experimental.', + defaultMessage: 'Try out features that are in progress or in technical preview.', }), getResetToDefaultLabel: () => i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { diff --git a/src/plugins/vis_types/timelion/server/ui_settings.ts b/src/plugins/vis_types/timelion/server/ui_settings.ts index 40907b0271487..2dff4f013c25c 100644 --- a/src/plugins/vis_types/timelion/server/ui_settings.ts +++ b/src/plugins/vis_types/timelion/server/ui_settings.ts @@ -14,7 +14,7 @@ import { UI_SETTINGS } from '../common/constants'; import { configSchema } from '../config'; const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { - defaultMessage: 'experimental', + defaultMessage: 'technical preview', }); export function getUiSettings( diff --git a/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx b/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx index 6d6ed0a6c1cc8..e98e0d65bd796 100644 --- a/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx +++ b/src/plugins/vis_types/vega/public/components/experimental_map_vis_info.tsx @@ -20,7 +20,9 @@ export const ExperimentalMapLayerInfo = () => ( title={ { <> { className="visListingTable__experimentalIcon" label="E" title={i18n.translate('visualizations.listing.experimentalTitle', { - defaultMessage: 'Experimental', + defaultMessage: 'Technical preview', })} tooltipContent={i18n.translate('visualizations.listing.experimentalTooltip', { defaultMessage: - 'This visualization might be changed or removed in a future release and is not subject to the support SLA.', + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', })} /> ); diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx index a8080cac2c06c..bd6e64597ec8f 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx @@ -238,10 +238,10 @@ const ToolsGroup = ({ visType, onVisTypeSelected, showExperimental }: VisCardPro iconType="beaker" tooltipContent={i18n.translate('visualizations.newVisWizard.experimentalTooltip', { defaultMessage: - 'This visualization might be changed or removed in a future release and is not subject to the support SLA.', + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', })} label={i18n.translate('visualizations.newVisWizard.experimentalTitle', { - defaultMessage: 'Experimental', + defaultMessage: 'Technical preview', })} /> diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index 97c3e4bc19387..7ad8a44200bc1 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -55,12 +55,12 @@ export class VisualizationsPlugin core.uiSettings.register({ [VISUALIZE_ENABLE_LABS_SETTING]: { name: i18n.translate('visualizations.advancedSettings.visualizeEnableLabsTitle', { - defaultMessage: 'Enable experimental visualizations', + defaultMessage: 'Enable technical preview visualizations', }), value: true, description: i18n.translate('visualizations.advancedSettings.visualizeEnableLabsText', { - defaultMessage: `Allows users to create, view, and edit experimental visualizations. If disabled, - only visualizations that are considered production-ready are available to the user.`, + defaultMessage: `Allows users to create, view, and edit visualizations that are in technical preview. + If disabled, only visualizations that are considered production-ready are available to the user.`, }), category: ['visualization'], schema: schema.boolean(), diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 3565a978b5f07..18e405f70c859 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -19,7 +19,7 @@ import { getActionsConfigurationUtilities } from './actions_config'; import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '../../licensing/server/mocks'; import { httpServerMock, loggingSystemMock } from '../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../security/server/audit/mocks'; import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock'; import { @@ -72,7 +72,7 @@ const authorization = actionsAuthorizationMock.create(); const executionEnqueuer = jest.fn(); const ephemeralExecutionEnqueuer = jest.fn(); const request = httpServerMock.createKibanaRequest(); -const auditLogger = auditServiceMock.create().asScoped(request); +const auditLogger = auditLoggerMock.create(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const logger = loggingSystemMock.create().get() as jest.Mocked; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index cd452ce6df571..fc38d540e0274 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { RecoveredActionGroup } from '../../../common'; import { RegistryRuleType } from '../../rule_type_registry'; @@ -27,7 +26,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 4061917b15197..1485d8b639159 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -16,8 +16,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { RecoveredActionGroup } from '../../../common'; import { getDefaultRuleMonitoring } from '../../task_runner/task_runner'; @@ -34,7 +33,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v8.0.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts index 4847314a2ceae..cfb684f4241ee 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup } from './lib'; const taskManager = taskManagerMock.createStart(); @@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index 9c3e5872c76e1..afb67ef47c62f 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -15,8 +15,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { InvalidatePendingApiKey } from '../../types'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { eventLoggerMock } from '../../../../event_log/server/event_logger.mock'; import { TaskStatus } from '../../../../task_manager/server'; @@ -31,7 +30,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const eventLogger = eventLoggerMock.create(); const kibanaVersion = 'v7.10.0'; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index afa7db98cab08..c2826974fb8e8 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -15,8 +15,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { InvalidatePendingApiKey } from '../../types'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -26,7 +25,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index 2729b42490d9f..c0079d7787281 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -16,8 +16,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { RecoveredActionGroup } from '../../../common'; import { RegistryRuleType } from '../../rule_type_registry'; @@ -28,7 +27,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts index d61796431a1ee..c704c3eec1241 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { RecoveredActionGroup } from '../../../common'; @@ -25,7 +24,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts index 883885c28aff7..76bc313bed024 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); @@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts index cb19066500bde..4a77bfd38ef83 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); @@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts index 8102dcedbd780..bfa2a10189a05 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { RecoveredActionGroup } from '../../../common'; @@ -25,7 +24,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts index 4d08da7289e4c..e05ea363452e8 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); @@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts index 8319c486e965f..fa228478a39d8 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); @@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 55ffc49fd3394..f59b69c0b3fbf 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -20,8 +20,7 @@ import { AlertingAuthorization } from '../../authorization/alerting_authorizatio import { resolvable } from '../../test_utils'; import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; jest.mock('../../../../../../src/core/server/saved_objects/service/lib/utils', () => ({ @@ -36,7 +35,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts index 0ee6d1162eca8..0b335a13d07e3 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts @@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { httpServerMock } from '../../../../../../src/core/server/mocks'; -import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { InvalidatePendingApiKey } from '../../types'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -25,7 +24,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); -const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); +const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v7.10.0'; const rulesClientParams: jest.Mocked = { diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index f00487cdc4082..6d20faae89a10 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -16,7 +16,6 @@ import { EuiSpacer, EuiIcon, EuiTitle, - EuiBetaBadge, EuiBadge, EuiToolTip, EuiSwitch, @@ -451,31 +450,6 @@ export function FailedTransactionsCorrelations({ - - - - - diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 20f907e03fc37..8a2d837857060 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -302,14 +302,14 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label={i18n.translate( 'xpack.apm.serviceDetails.profilingTabExperimentalLabel', { - defaultMessage: 'Experimental', + defaultMessage: 'Technical preview', } )} tooltipContent={i18n.translate( 'xpack.apm.serviceDetails.profilingTabExperimentalDescription', { defaultMessage: - 'Profiling is highly experimental and for internal use only.', + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', } )} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx index b9f935caa98c0..6052edfd9ee77 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx @@ -110,7 +110,7 @@ describe('getAlertAnnotations', () => { setSelectedAlertId, theme, })![0].props.dataValues[0].header - ).toEqual('Alert - Experimental'); + ).toEqual('Alert - Technical preview'); }); it('uses the reason in the annotation details', () => { @@ -191,7 +191,7 @@ describe('getAlertAnnotations', () => { setSelectedAlertId, theme, })![0].props.dataValues[0].header - ).toEqual('Warning Alert - Experimental'); + ).toEqual('Warning Alert - Technical preview'); }); }); @@ -224,7 +224,7 @@ describe('getAlertAnnotations', () => { setSelectedAlertId, theme, })![0].props.dataValues[0].header - ).toEqual('Critical Alert - Experimental'); + ).toEqual('Critical Alert - Technical preview'); }); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index ca2d0351c8135..b4cd13d4615ed 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -107,7 +107,7 @@ export function getAlertAnnotations({ const color = getAlertColor({ severityLevel, theme }); const experimentalLabel = i18n.translate( 'xpack.apm.alertAnnotationTooltipExperimentalText', - { defaultMessage: 'Experimental' } + { defaultMessage: 'Technical preview' } ); const header = `${getAlertHeader({ severityLevel, diff --git a/x-pack/plugins/canvas/server/ui_settings.ts b/x-pack/plugins/canvas/server/ui_settings.ts index 8c7dc9a095872..3e7de1dbb7d79 100644 --- a/x-pack/plugins/canvas/server/ui_settings.ts +++ b/x-pack/plugins/canvas/server/ui_settings.ts @@ -21,7 +21,7 @@ export const getUISettings = (): Record> => ({ }), description: i18n.translate('xpack.canvas.labs.enableLabsDescription', { defaultMessage: - 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable experimental features in Canvas.', + 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable technical preview features in Canvas.', }), value: false, type: 'boolean', diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts index c2f00e8cfff05..3c465a6f843c0 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AuditLogger } from '../../../../plugins/security/server'; +import { auditLoggerMock } from '../../../../plugins/security/server/audit/mocks'; import { Operations } from '.'; import { AuthorizationAuditLogger } from './audit_logger'; import { ReadOperations } from './types'; @@ -30,11 +30,7 @@ describe('audit_logger', () => { }); describe('log function', () => { - const mockLogger: jest.Mocked = { - log: jest.fn(), - enabled: true, - }; - + const mockLogger = auditLoggerMock.create(); let logger: AuthorizationAuditLogger; beforeEach(() => { diff --git a/x-pack/plugins/cases/server/authorization/authorization.test.ts b/x-pack/plugins/cases/server/authorization/authorization.test.ts index 693277161c330..650119bab0079 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.test.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.test.ts @@ -14,6 +14,7 @@ import { AuthorizationAuditLogger } from './audit_logger'; import { KibanaRequest } from 'kibana/server'; import { KibanaFeature } from '../../../../plugins/features/common'; import { AuditLogger, SecurityPluginStart } from '../../../security/server'; +import { auditLoggerMock } from '../../../security/server/audit/mocks'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; describe('authorization', () => { @@ -22,10 +23,7 @@ describe('authorization', () => { beforeEach(() => { request = httpServerMock.createKibanaRequest(); - mockLogger = { - log: jest.fn(), - enabled: true, - }; + mockLogger = auditLoggerMock.create(); }); describe('create', () => { diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index 7444159a00bb0..7f966a471c725 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -42,6 +42,7 @@ export class AlertService { const res = await this.scopedClusterClient.search({ index: indices, + ignore_unavailable: true, query: { ids: { values: ids } }, size: 0, aggregations: builtAggs, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.ts index 547d920045b90..b08e43ec3d4c5 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/release_badge.ts @@ -14,7 +14,7 @@ export const RELEASE_BADGE_LABEL: { [key in Exclude]: str defaultMessage: 'Beta', }), experimental: i18n.translate('xpack.fleet.epm.releaseBadge.experimentalLabel', { - defaultMessage: 'Experimental', + defaultMessage: 'Technical preview', }), }; @@ -23,6 +23,7 @@ export const RELEASE_BADGE_DESCRIPTION: { [key in Exclude defaultMessage: 'This integration is not recommended for use in production environments.', }), experimental: i18n.translate('xpack.fleet.epm.releaseBadge.experimentalDescription', { - defaultMessage: 'This integration may have breaking changes or be removed in a future release.', + defaultMessage: + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', }), }; diff --git a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts new file mode 100644 index 0000000000000..bb34dc3258d05 --- /dev/null +++ b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { spawn } from 'child_process'; +import type { ChildProcess } from 'child_process'; + +import fetch from 'node-fetch'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function useDockerRegistry() { + const packageRegistryPort = process.env.FLEET_PACKAGE_REGISTRY_PORT || '8081'; + + if (!packageRegistryPort.match(/^[0-9]{4}/)) { + throw new Error('Invalid FLEET_PACKAGE_REGISTRY_PORT'); + } + + let dockerProcess: ChildProcess | undefined; + async function startDockerRegistryServer() { + const dockerImage = `docker.elastic.co/package-registry/distribution@sha256:de952debe048d903fc73e8a4472bb48bb95028d440cba852f21b863d47020c61`; + + const args = ['run', '--rm', '-p', `${packageRegistryPort}:8080`, dockerImage]; + + dockerProcess = spawn('docker', args, { stdio: 'inherit' }); + + let isExited = dockerProcess.exitCode !== null; + dockerProcess.once('exit', () => { + isExited = true; + }); + + let retries = 0; + while (!isExited && retries++ <= 20) { + try { + const res = await fetch(`http://localhost:${packageRegistryPort}/`); + if (res.status === 200) { + return; + } + } catch (err) { + // swallow errors + } + + await delay(3000); + } + + dockerProcess.kill(); + throw new Error('Unable to setup docker registry'); + } + + async function cleanupDockerRegistryServer() { + if (dockerProcess && !dockerProcess.killed) { + dockerProcess.kill(); + } + } + + beforeAll(async () => { + jest.setTimeout(5 * 60 * 1000); // 5 minutes timeout + await startDockerRegistryServer(); + }); + + afterAll(async () => { + await cleanupDockerRegistryServer(); + }); + + return `http://localhost:${packageRegistryPort}`; +} diff --git a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts index 9efbacfae17bf..4a2b4f2495e96 100644 --- a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts @@ -14,6 +14,8 @@ import type { HttpMethod } from 'src/core/test_helpers/kbn_server'; import type { AgentPolicySOAttributes } from '../types'; +import { useDockerRegistry } from './docker_registry_helper'; + const logFilePath = Path.join(__dirname, 'logs.log'); type Root = ReturnType; @@ -46,6 +48,8 @@ describe('Fleet preconfiguration reset', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let kbnServer: kbnTestServer.TestKibanaUtils; + const registryUrl = useDockerRegistry(); + const startServers = async () => { const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t) => jest.setTimeout(t), @@ -63,6 +67,7 @@ describe('Fleet preconfiguration reset', () => { { xpack: { fleet: { + registryUrl, packages: [ { name: 'fleet_server', @@ -195,8 +200,7 @@ describe('Fleet preconfiguration reset', () => { await stopServers(); }); - // FLAKY: https://github.com/elastic/kibana/issues/123103 - describe.skip('Reset all policy', () => { + describe('Reset all policy', () => { it('Works and reset all preconfigured policies', async () => { const resetAPI = getSupertestWithAdminUser( kbnServer.root, @@ -225,9 +229,7 @@ describe('Fleet preconfiguration reset', () => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/123104 - // FLAKY: https://github.com/elastic/kibana/issues/123105 - describe.skip('Reset one preconfigured policy', () => { + describe('Reset one preconfigured policy', () => { const POLICY_ID = 'test-12345'; it('Works and reset one preconfigured policies if the policy is already deleted (with a ghost package policy)', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index fe054edfb2917..4e75981f6f45e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -366,7 +366,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 35c3ad044216c..50b21433ebf2c 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -5,12 +5,12 @@ * 2.0. */ -import _ from 'lodash'; import { CoreStart, Logger } from 'src/core/server'; import type { DataRequestHandlerContext } from 'src/plugins/data/server'; import { Stream } from 'stream'; import { isAbortError } from './util'; import { makeExecutionContext } from '../../common/execution_context'; +import { Field, mergeFields } from './merge_fields'; export async function getEsTile({ url, @@ -39,14 +39,19 @@ export async function getEsTile({ }): Promise { try { const path = `/${encodeURIComponent(index)}/_mvt/${geometryFieldName}/${z}/${x}/${y}`; - let fields = _.uniq(requestBody.docvalue_fields.concat(requestBody.stored_fields)); - fields = fields.filter((f) => f !== geometryFieldName); + const body = { grid_precision: 0, // no aggs exact_bounds: true, extent: 4096, // full resolution, query: requestBody.query, - fields, + fields: mergeFields( + [ + requestBody.docvalue_fields as Field[] | undefined, + requestBody.stored_fields as Field[] | undefined, + ], + [geometryFieldName] + ), runtime_mappings: requestBody.runtime_mappings, track_total_hits: requestBody.size + 1, }; diff --git a/x-pack/plugins/maps/server/mvt/merge_fields.ts b/x-pack/plugins/maps/server/mvt/merge_fields.ts new file mode 100644 index 0000000000000..e371f3ff0715b --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/merge_fields.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// can not use "import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey" +// SearchRequest is incorrectly typed and does not support Field as object +// https://github.com/elastic/elasticsearch-js/issues/1615 +export type Field = + | string + | { + field: string; + format: string; + }; + +export function mergeFields( + fieldsList: Array, + excludeNames: string[] +): Field[] { + const fieldNames: string[] = []; + const mergedFields: Field[] = []; + + fieldsList.forEach((fields) => { + if (!fields) { + return; + } + + fields.forEach((field) => { + const fieldName = typeof field === 'string' ? field : field.field; + if (!excludeNames.includes(fieldName) && !fieldNames.includes(fieldName)) { + fieldNames.push(fieldName); + mergedFields.push(field); + } + }); + }); + + return mergedFields; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts index 1c249ee1b5037..2b3a4a2dcbb50 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { EuiDataGridSorting } from '@elastic/eui'; - -import { multiColumnSortFactory } from './common'; +import { MultiColumnSorter, multiColumnSortFactory } from './common'; describe('Data Frame Analytics: Data Grid Common', () => { test('multiColumnSortFactory()', () => { @@ -18,7 +16,7 @@ describe('Data Frame Analytics: Data Grid Common', () => { { s: 'b', n: 4 }, ]; - const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; + const sortingColumns1: MultiColumnSorter[] = [{ id: 's', direction: 'desc', type: 'number' }]; const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); data.sort(multiColumnSort1); @@ -29,9 +27,9 @@ describe('Data Frame Analytics: Data Grid Common', () => { { s: 'a', n: 2 }, ]); - const sortingColumns2: EuiDataGridSorting['columns'] = [ - { id: 's', direction: 'asc' }, - { id: 'n', direction: 'desc' }, + const sortingColumns2: MultiColumnSorter[] = [ + { id: 's', direction: 'asc', type: 'number' }, + { id: 'n', direction: 'desc', type: 'number' }, ]; const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); data.sort(multiColumnSort2); @@ -43,9 +41,9 @@ describe('Data Frame Analytics: Data Grid Common', () => { { s: 'b', n: 3 }, ]); - const sortingColumns3: EuiDataGridSorting['columns'] = [ - { id: 'n', direction: 'desc' }, - { id: 's', direction: 'desc' }, + const sortingColumns3: MultiColumnSorter[] = [ + { id: 'n', direction: 'desc', type: 'number' }, + { id: 's', direction: 'desc', type: 'number' }, ]; const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); data.sort(multiColumnSort3); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index d49442b9864d4..31979000f4a60 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -9,11 +9,7 @@ import moment from 'moment-timezone'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { useEffect, useMemo } from 'react'; -import { - EuiDataGridCellValueElementProps, - EuiDataGridSorting, - EuiDataGridStyle, -} from '@elastic/eui'; +import { EuiDataGridCellValueElementProps, EuiDataGridStyle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -178,7 +174,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results export const NON_AGGREGATABLE = 'non-aggregatable'; export const getDataGridSchemaFromESFieldType = ( - fieldType: ES_FIELD_TYPES | undefined | estypes.MappingRuntimeField['type'] + fieldType: ES_FIELD_TYPES | undefined | estypes.MappingRuntimeField['type'] | 'number' ): string | undefined => { // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. @@ -204,6 +200,7 @@ export const getDataGridSchemaFromESFieldType = ( case ES_FIELD_TYPES.LONG: case ES_FIELD_TYPES.SCALED_FLOAT: case ES_FIELD_TYPES.SHORT: + case 'number': schema = 'numeric'; break; // keep schema undefined for text based columns @@ -417,6 +414,16 @@ export const useRenderCellValue = ( return renderCellValue; }; +// Value can be nested or the fieldName itself might contain other special characters like `.` +export const getNestedOrEscapedVal = (obj: any, sortId: string) => + getNestedProperty(obj, sortId, null) ?? obj[sortId]; + +export interface MultiColumnSorter { + id: string; + direction: 'asc' | 'desc'; + type: string; +} + /** * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. * `sortFn()` is recursive to support sorting on multiple columns. @@ -424,17 +431,17 @@ export const useRenderCellValue = ( * @param sortingColumns - The EUI data grid sorting configuration * @returns The sorting function which can be used with an array's sort() function. */ -export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { - const isString = (arg: any): arg is string => { - return typeof arg === 'string'; - }; - +export const multiColumnSortFactory = (sortingColumns: MultiColumnSorter[]) => { const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { const sort = sortingColumns[sortingColumnIndex]; - const aValue = getNestedProperty(a, sort.id, null); - const bValue = getNestedProperty(b, sort.id, null); - if (typeof aValue === 'number' && typeof bValue === 'number') { + // Value can be nested or the fieldName itself might contain `.` + let aValue = getNestedOrEscapedVal(a, sort.id); + let bValue = getNestedOrEscapedVal(b, sort.id); + + if (sort.type === 'number') { + aValue = aValue ?? 0; + bValue = bValue ?? 0; if (aValue < bValue) { return sort.direction === 'asc' ? -1 : 1; } @@ -443,7 +450,10 @@ export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['colum } } - if (isString(aValue) && isString(bValue)) { + if (sort.type === 'string') { + aValue = aValue ?? ''; + bValue = bValue ?? ''; + if (aValue.localeCompare(bValue) === -1) { return sort.direction === 'asc' ? -1 : 1; } diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index b2bd1ff228923..8b09617aa817e 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -12,6 +12,7 @@ export { getFieldsFromKibanaIndexPattern, getCombinedRuntimeMappings, multiColumnSortFactory, + getNestedOrEscapedVal, showDataGridColumnChartErrorMessageToast, useRenderCellValue, getProcessedFields, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index 633c3d9aab002..55c12338b8fd4 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -87,15 +87,15 @@ export const useDataGrid = ( const onSort: OnSort = useCallback( (sc) => { // Check if an unsupported column type for sorting was selected. - const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const updatedInvalidSortingColumns = sc.reduce((arr, current) => { const columnType = columns.find((dgc) => dgc.id === current.id); if (columnType?.schema === 'json') { arr.push(current.id); } return arr; }, []); - setInvalidSortingColumnns(updatedInvalidSortingColumnns); - if (updatedInvalidSortingColumnns.length === 0) { + setInvalidSortingColumnns(updatedInvalidSortingColumns); + if (updatedInvalidSortingColumns.length === 0) { setSortingColumns(sc); } }, diff --git a/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx index 21d30ef76c458..a8f92e8ae9f71 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/trained_models/models_list.tsx @@ -62,7 +62,7 @@ const PageWrapper: FC = ({ location, deps }) => { = ({ location, deps }) => { 'xpack.ml.navMenu.trainedModelsTabBetaTooltipContent', { defaultMessage: - "Model Management is an experimental feature and subject to change. We'd love to hear your feedback.", + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', } )} tooltipPosition={'right'} diff --git a/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx index 88df1e0b07f58..33791f1e2aa81 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/trained_models/nodes_list.tsx @@ -60,7 +60,7 @@ const PageWrapper: FC = ({ location, deps }) => { = ({ location, deps }) => { 'xpack.ml.navMenu.trainedModelsTabBetaTooltipContent', { defaultMessage: - "Model Management is an experimental feature and subject to change. We'd love to hear your feedback.", + 'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.', } )} tooltipPosition={'right'} diff --git a/x-pack/plugins/observability/public/components/shared/experimental_badge.tsx b/x-pack/plugins/observability/public/components/shared/experimental_badge.tsx index a99187271806a..eb755200949c3 100644 --- a/x-pack/plugins/observability/public/components/shared/experimental_badge.tsx +++ b/x-pack/plugins/observability/public/components/shared/experimental_badge.tsx @@ -13,11 +13,11 @@ export function ExperimentalBadge() { return ( ); diff --git a/x-pack/plugins/observability/public/pages/alerts/components/alerts_disclaimer.tsx b/x-pack/plugins/observability/public/pages/alerts/components/alerts_disclaimer.tsx index 4b465a1091965..9a07c1b3cd605 100644 --- a/x-pack/plugins/observability/public/pages/alerts/components/alerts_disclaimer.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/alerts_disclaimer.tsx @@ -35,14 +35,13 @@ export function AlertsDisclaimer() { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts index af531e8ae8e12..c8660fc69c224 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client_factory.test.ts @@ -11,7 +11,7 @@ import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_f import { ElasticsearchClient, KibanaRequest } from 'src/core/server'; import { loggingSystemMock } from 'src/core/server/mocks'; import { securityMock } from '../../../security/server/mocks'; -import { AuditLogger } from '../../../security/server'; +import { auditLoggerMock } from '../../../security/server/audit/mocks'; import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock'; @@ -44,10 +44,7 @@ const fakeRequest = { }, } as unknown as Request; -const auditLogger = { - log: jest.fn(), - enabled: true, -} as jest.Mocked; +const auditLogger = auditLoggerMock.create(); describe('AlertsClientFactory', () => { beforeEach(() => { diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index 09861278cd5d5..3f89506c0a1f4 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -17,16 +17,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; -import { AuditLogger } from '../../../../security/server'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); -const auditLogger = { - log: jest.fn(), - enabled: true, -} as jest.Mocked; +const auditLogger = auditLoggerMock.create(); const alertsClientParams: jest.Mocked = { logger: loggingSystemMock.create().get(), diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts index bfff95b5d601b..285d318749786 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/find_alerts.test.ts @@ -16,16 +16,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; -import { AuditLogger } from '../../../../security/server'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); -const auditLogger = { - log: jest.fn(), - enabled: true, -} as jest.Mocked; +const auditLogger = auditLoggerMock.create(); const alertsClientParams: jest.Mocked = { logger: loggingSystemMock.create().get(), diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 0c74cc1463410..acc6e37099fdd 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -17,16 +17,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; -import { AuditLogger } from '../../../../security/server'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); -const auditLogger = { - log: jest.fn(), - enabled: true, -} as jest.Mocked; +const auditLogger = auditLoggerMock.create(); const alertsClientParams: jest.Mocked = { logger: loggingSystemMock.create().get(), diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 0dcfc602bc281..2778d4da0f089 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -16,16 +16,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock'; -import { AuditLogger } from '../../../../security/server'; +import { auditLoggerMock } from '../../../../security/server/audit/mocks'; import { AlertingAuthorizationEntity } from '../../../../alerting/server'; import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock'; const alertingAuthMock = alertingAuthorizationMock.create(); const esClientMock = elasticsearchClientMock.createElasticsearchClient(); -const auditLogger = { - log: jest.fn(), - enabled: true, -} as jest.Mocked; +const auditLogger = auditLoggerMock.create(); const alertsClientParams: jest.Mocked = { logger: loggingSystemMock.create().get(), diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts index 0a7337e453274..7ebc07a37b5c9 100644 --- a/x-pack/plugins/security/server/audit/audit_events.test.ts +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -239,6 +239,7 @@ describe('#userLoginEvent', () => { authenticationResult: AuthenticationResult.succeeded(mockAuthenticatedUser()), authenticationProvider: 'basic1', authenticationType: 'basic', + sessionId: '123', }) ).toMatchInlineSnapshot(` Object { @@ -255,6 +256,7 @@ describe('#userLoginEvent', () => { "authentication_realm": "native1", "authentication_type": "basic", "lookup_realm": "native1", + "session_id": "123", "space_id": undefined, }, "message": "User [user] has logged in using basic provider [name=basic1]", @@ -293,6 +295,7 @@ describe('#userLoginEvent', () => { "authentication_realm": undefined, "authentication_type": "basic", "lookup_realm": undefined, + "session_id": undefined, "space_id": undefined, }, "message": "Failed attempt to login using basic provider [name=basic1]", diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 37b2cecfa55c1..c896b6cea9947 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -97,12 +97,14 @@ export interface UserLoginParams { authenticationResult: AuthenticationResult; authenticationProvider?: string; authenticationType?: string; + sessionId?: string; } export function userLoginEvent({ authenticationResult, authenticationProvider, authenticationType, + sessionId, }: UserLoginParams): AuditEvent { return { message: authenticationResult.user @@ -119,6 +121,7 @@ export function userLoginEvent({ }, kibana: { space_id: undefined, // Ensure this does not get populated by audit service + session_id: sessionId, authentication_provider: authenticationProvider, authentication_type: authenticationType, authentication_realm: authenticationResult.user?.authentication_realm.name, diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/mocks.ts similarity index 55% rename from x-pack/plugins/security/server/audit/index.mock.ts rename to x-pack/plugins/security/server/audit/mocks.ts index 6ac9108b51a83..6485818e7fc58 100644 --- a/x-pack/plugins/security/server/audit/index.mock.ts +++ b/x-pack/plugins/security/server/audit/mocks.ts @@ -5,20 +5,23 @@ * 2.0. */ -import type { AuditService } from './audit_service'; +import type { AuditLogger, AuditService } from './audit_service'; + +export const auditLoggerMock = { + create() { + return { + log: jest.fn(), + enabled: true, + } as jest.Mocked; + }, +}; export const auditServiceMock = { create() { return { getLogger: jest.fn(), - asScoped: jest.fn().mockReturnValue({ - log: jest.fn(), - enabled: true, - }), - withoutRequest: { - log: jest.fn(), - enabled: true, - }, + asScoped: jest.fn().mockReturnValue(auditLoggerMock.create()), + withoutRequest: auditLoggerMock.create(), } as jest.Mocked>; }, }; diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 7d9a36f489b7c..b740a2a2db085 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -39,7 +39,7 @@ import type { AuthenticatedUser, SecurityLicense } from '../../common'; import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; import type { AuditServiceSetup } from '../audit'; -import { auditServiceMock } from '../audit/index.mock'; +import { auditServiceMock } from '../audit/mocks'; import type { ConfigType } from '../config'; import { ConfigSchema, createConfig } from '../config'; import type { SecurityFeatureUsageServiceStart } from '../feature_usage'; diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index f0ff164cbbc5a..1559a6fa4a5d7 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -27,7 +27,8 @@ import { import type { SecurityLicenseFeatures } from '../../common/licensing'; import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; -import { auditServiceMock } from '../audit/index.mock'; +import type { AuditLogger } from '../audit'; +import { auditLoggerMock, auditServiceMock } from '../audit/mocks'; import { ConfigSchema, createConfig } from '../config'; import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; import { securityMock } from '../mocks'; @@ -39,6 +40,7 @@ import { Authenticator } from './authenticator'; import { DeauthenticationResult } from './deauthentication_result'; import type { BasicAuthenticationProvider, SAMLAuthenticationProvider } from './providers'; +let auditLogger: AuditLogger; function getMockOptions({ providers, http = {}, @@ -48,8 +50,11 @@ function getMockOptions({ http?: Partial; selector?: AuthenticatorOptions['config']['authc']['selector']; } = {}) { + const auditService = auditServiceMock.create(); + auditLogger = auditLoggerMock.create(); + auditService.asScoped.mockReturnValue(auditLogger); return { - audit: auditServiceMock.create(), + audit: auditService, getCurrentUser: jest.fn(), clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, @@ -66,6 +71,26 @@ function getMockOptions({ }; } +interface ExpectedAuditEvent { + action: string; + outcome?: string; + kibana?: Record; +} + +function expectAuditEvents(...events: ExpectedAuditEvent[]) { + expect(auditLogger.log).toHaveBeenCalledTimes(events.length); + for (let i = 0; i < events.length; i++) { + const { action, outcome, kibana } = events[i]; + expect(auditLogger.log).toHaveBeenNthCalledWith( + i + 1, + expect.objectContaining({ + event: { action, category: ['authentication'], ...(outcome && { outcome }) }, + ...(kibana && { kibana }), + }) + ); + } +} + describe('Authenticator', () => { let mockBasicAuthenticationProvider: jest.Mocked>; beforeEach(() => { @@ -262,16 +287,10 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessVal: SessionValue; - const auditLogger = { - log: jest.fn(), - enabled: true, - }; beforeEach(() => { - auditLogger.log.mockClear(); mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); mockOptions.session.get.mockResolvedValue(null); - mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); authenticator = new Authenticator(mockOptions); @@ -281,6 +300,7 @@ describe('Authenticator', () => { await expect(authenticator.login(undefined as any, undefined as any)).rejects.toThrowError( 'Request should be a valid "KibanaRequest" instance, was [undefined].' ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('fails if login attempt is not provided or invalid.', async () => { @@ -304,6 +324,7 @@ describe('Authenticator', () => { ).rejects.toThrowError( 'Login attempt should be an object with non-empty "provider.type" or "provider.name" property.' ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('fails if an authentication provider fails.', async () => { @@ -317,6 +338,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + expectAuditEvents({ action: 'user_login', outcome: 'failure' }); }); it('returns user that authentication provider returns.', async () => { @@ -332,38 +354,49 @@ describe('Authenticator', () => { ).resolves.toEqual( AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) ); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); - it('adds audit event when successful.', async () => { - const request = httpServerMock.createKibanaRequest(); - const user = mockAuthenticatedUser(); - mockBasicAuthenticationProvider.login.mockResolvedValue( - AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) - ); - await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + describe('user_login audit events', () => { + // Every other test case includes audit event assertions, but the user_login event is a bit special. + // We have these separate, detailed test cases to ensure that the session ID is included for user_login success events. + // This allows us to keep audit event assertions in the other test cases simpler. - expect(auditLogger.log).toHaveBeenCalledTimes(1); - expect(auditLogger.log).toHaveBeenCalledWith( - expect.objectContaining({ - event: { action: 'user_login', category: ['authentication'], outcome: 'success' }, - }) - ); - }); + it('adds audit event with session ID when successful.', async () => { + const request = httpServerMock.createKibanaRequest(); + const user = mockAuthenticatedUser(); + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.succeeded(user, { + authHeaders: { authorization: 'Basic .....' }, + state: 'foo', // to ensure a new session is created + }) + ); + mockOptions.session.create.mockResolvedValue({ ...mockSessVal, sid: '123' }); + await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); - it('adds audit event when not successful.', async () => { - const request = httpServerMock.createKibanaRequest(); - const failureReason = new Error('Not Authorized'); - mockBasicAuthenticationProvider.login.mockResolvedValue( - AuthenticationResult.failed(failureReason) - ); - await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + expect(mockOptions.session.create).toHaveBeenCalledTimes(1); + expectAuditEvents({ + action: 'user_login', + outcome: 'success', + kibana: expect.objectContaining({ authentication_type: 'basic', session_id: '123' }), + }); + }); - expect(auditLogger.log).toHaveBeenCalledTimes(1); - expect(auditLogger.log).toHaveBeenCalledWith( - expect.objectContaining({ - event: { action: 'user_login', category: ['authentication'], outcome: 'failure' }, - }) - ); + it('adds audit event without session ID when not successful.', async () => { + const request = httpServerMock.createKibanaRequest(); + const failureReason = new Error('Not Authorized'); + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.failed(failureReason) + ); + await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + + expect(mockOptions.session.create).not.toHaveBeenCalled(); + expectAuditEvents({ + action: 'user_login', + outcome: 'failure', + kibana: expect.objectContaining({ authentication_type: 'basic', session_id: undefined }), + }); + }); }); it('does not add audit event when not handled.', async () => { @@ -396,6 +429,7 @@ describe('Authenticator', () => { provider: mockSessVal.provider, state: { authorization }, }); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('returns `notHandled` if login attempt is targeted to not configured provider.', async () => { @@ -476,6 +510,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled(); expect(mockSAMLAuthenticationProvider1.login).not.toHaveBeenCalled(); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('tries to login only with the provider that has specified type', async () => { @@ -493,6 +528,7 @@ describe('Authenticator', () => { expect(mockSAMLAuthenticationProvider1.login.mock.invocationCallOrder[0]).toBeLessThan( mockSAMLAuthenticationProvider2.login.mock.invocationCallOrder[0] ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('returns as soon as provider handles request', async () => { @@ -528,6 +564,10 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled(); expect(mockSAMLAuthenticationProvider2.login).not.toHaveBeenCalled(); expect(mockSAMLAuthenticationProvider1.login).toHaveBeenCalledTimes(3); + expectAuditEvents( + { action: 'user_login', outcome: 'failure' }, + { action: 'user_login', outcome: 'success' } + ); }); it('provides session only if provider name matches', async () => { @@ -563,6 +603,7 @@ describe('Authenticator', () => { expect(mockSAMLAuthenticationProvider2.login.mock.invocationCallOrder[0]).toBeLessThan( mockSAMLAuthenticationProvider1.login.mock.invocationCallOrder[0] ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); }); @@ -595,6 +636,10 @@ describe('Authenticator', () => { expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('clears session if provider asked to do so in `succeeded` result.', async () => { @@ -615,6 +660,10 @@ describe('Authenticator', () => { expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('clears session if provider asked to do so in `redirected` result.', async () => { @@ -634,6 +683,7 @@ describe('Authenticator', () => { expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); describe('with Access Agreement', () => { @@ -670,6 +720,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.succeeded(mockUser)); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('does not redirect to Access Agreement if request cannot be handled', async () => { @@ -681,6 +732,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.notHandled()); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if authentication fails', async () => { @@ -695,6 +747,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.failed(failureReason)); + expectAuditEvents({ action: 'user_login', outcome: 'failure' }); }); it('does not redirect to Access Agreement if redirect is required to complete login', async () => { @@ -708,6 +761,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.redirectTo('/some-url', { state: 'some-state' })); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if user has already acknowledged it', async () => { @@ -724,6 +778,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' })); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('does not redirect to Access Agreement its own requests', async () => { @@ -737,6 +792,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' })); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('does not redirect to Access Agreement if it is not configured', async () => { @@ -752,6 +808,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' })); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('does not redirect to Access Agreement if license doesnt allow it.', async () => { @@ -768,6 +825,7 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' })); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('redirects to Access Agreement when needed.', async () => { @@ -793,6 +851,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('redirects to Access Agreement preserving redirect URL specified in login attempt.', async () => { @@ -822,6 +881,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('redirects to Access Agreement preserving redirect URL specified in the authentication result.', async () => { @@ -848,6 +908,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('redirects AJAX requests to Access Agreement when needed.', async () => { @@ -873,6 +934,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); }); @@ -909,6 +971,10 @@ describe('Authenticator', () => { await expect( authenticator.login(request, { provider: { type: 'basic' }, value: {} }) ).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' })); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('does not redirect to Overwritten Session if username and provider did not change', async () => { @@ -930,6 +996,7 @@ describe('Authenticator', () => { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); + expectAuditEvents({ action: 'user_login', outcome: 'success' }); }); it('does not redirect to Overwritten Session if session was unauthenticated before login', async () => { @@ -952,6 +1019,10 @@ describe('Authenticator', () => { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); + expectAuditEvents( + // We do not record a user_logout event for "intermediate" sessions that are deleted, only user_login for the new session + { action: 'user_login', outcome: 'success' } + ); }); it('redirects to Overwritten Session when username changes', async () => { @@ -977,6 +1048,10 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('redirects to Overwritten Session when provider changes', async () => { @@ -1005,6 +1080,10 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('redirects to Overwritten Session preserving redirect URL specified in login attempt.', async () => { @@ -1034,6 +1113,10 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('redirects to Overwritten Session preserving redirect URL specified in the authentication result.', async () => { @@ -1060,6 +1143,10 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); it('redirects AJAX requests to Overwritten Session when needed.', async () => { @@ -1085,6 +1172,10 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents( + { action: 'user_logout', outcome: 'unknown' }, + { action: 'user_login', outcome: 'success' } + ); }); }); }); @@ -1093,16 +1184,10 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessVal: SessionValue; - const auditLogger = { - log: jest.fn(), - enabled: true, - }; beforeEach(() => { - auditLogger.log.mockClear(); mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); mockOptions.session.get.mockResolvedValue(null); - mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); authenticator = new Authenticator(mockOptions); @@ -1112,6 +1197,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(undefined as any)).rejects.toThrowError( 'Request should be a valid "KibanaRequest" instance, was [undefined].' ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('fails if an authentication provider fails.', async () => { @@ -1125,6 +1211,7 @@ describe('Authenticator', () => { const authenticationResult = await authenticator.authenticate(request); expect(authenticationResult.failed()).toBe(true); expect(authenticationResult.error).toBe(failureReason); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('returns user that authentication provider returns.', async () => { @@ -1140,6 +1227,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('creates session whenever authentication provider returns state for system API requests', async () => { @@ -1163,6 +1251,7 @@ describe('Authenticator', () => { provider: mockSessVal.provider, state: { authorization }, }); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('creates session whenever authentication provider returns state for non-system API requests', async () => { @@ -1186,6 +1275,7 @@ describe('Authenticator', () => { provider: mockSessVal.provider, state: { authorization }, }); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not extend session for system API calls.', async () => { @@ -1207,6 +1297,7 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('extends session for non-system API calls.', async () => { @@ -1229,6 +1320,7 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { @@ -1250,6 +1342,7 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => { @@ -1271,6 +1364,7 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('replaces existing session with the one returned by authentication provider for system API requests', async () => { @@ -1297,6 +1391,7 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => { @@ -1323,6 +1418,7 @@ describe('Authenticator', () => { expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => { @@ -1342,11 +1438,12 @@ describe('Authenticator', () => { AuthenticationResult.failed(failureReason) ); - expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => { @@ -1366,11 +1463,12 @@ describe('Authenticator', () => { AuthenticationResult.failed(failureReason) ); - expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('clears session if provider requested it via setting state to `null`.', async () => { @@ -1385,31 +1483,12 @@ describe('Authenticator', () => { AuthenticationResult.redirectTo('some-url', { state: null }) ); - expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - }); - - it('adds audit event when invalidating session.', async () => { - const request = httpServerMock.createKibanaRequest(); - - mockBasicAuthenticationProvider.authenticate.mockResolvedValue( - AuthenticationResult.redirectTo('some-url', { state: null }) - ); - mockOptions.session.get.mockResolvedValue(mockSessVal); - - await expect(authenticator.authenticate(request)).resolves.toEqual( - AuthenticationResult.redirectTo('some-url', { state: null }) - ); - - expect(auditLogger.log).toHaveBeenCalledTimes(1); - expect(auditLogger.log).toHaveBeenCalledWith( - expect.objectContaining({ - event: { action: 'user_logout', category: ['authentication'], outcome: 'unknown' }, - }) - ); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('does not clear session if provider can not handle system API request authentication with active session.', async () => { @@ -1423,10 +1502,11 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); - expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => { @@ -1440,10 +1520,11 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); - expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); + expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); describe('with Login Selector', () => { @@ -1464,6 +1545,7 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect AJAX requests to Login Selector', async () => { @@ -1473,6 +1555,7 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Login Selector if request has `Authorization` header', async () => { @@ -1484,6 +1567,7 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Login Selector if it is not enabled', async () => { @@ -1495,6 +1579,7 @@ describe('Authenticator', () => { AuthenticationResult.notHandled() ); expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('redirects to the Login Selector when needed.', async () => { @@ -1513,6 +1598,7 @@ describe('Authenticator', () => { ) ); expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('redirects to the Login Selector with auth provider hint when needed.', async () => { @@ -1536,6 +1622,7 @@ describe('Authenticator', () => { ); expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); }); @@ -1565,6 +1652,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect AJAX requests to Access Agreement', async () => { @@ -1574,6 +1662,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if request cannot be handled', async () => { @@ -1587,6 +1676,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.notHandled() ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if authentication fails', async () => { @@ -1601,6 +1691,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.failed(failureReason) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if redirect is required to complete authentication', async () => { @@ -1614,6 +1705,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.redirectTo('/some-url') ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if user has already acknowledged it', async () => { @@ -1626,6 +1718,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement its own requests', async () => { @@ -1635,6 +1728,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if it is not configured', async () => { @@ -1646,6 +1740,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Access Agreement if license doesnt allow it.', async () => { @@ -1658,6 +1753,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('redirects to Access Agreement when needed.', async () => { @@ -1677,6 +1773,7 @@ describe('Authenticator', () => { { user: mockUser, authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' } } ) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); }); @@ -1712,6 +1809,7 @@ describe('Authenticator', () => { await expect(authenticator.authenticate(request)).resolves.toEqual( AuthenticationResult.succeeded(mockUser) ); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('does not redirect AJAX requests to Overwritten Session', async () => { @@ -1731,6 +1829,7 @@ describe('Authenticator', () => { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('does not redirect to Overwritten Session if username and provider did not change', async () => { @@ -1750,6 +1849,7 @@ describe('Authenticator', () => { authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' }, }) ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('redirects to Overwritten Session when username changes', async () => { @@ -1773,6 +1873,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('redirects to Overwritten Session when provider changes', async () => { @@ -1799,6 +1900,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('redirects to Overwritten Session preserving redirect URL specified in the authentication result.', async () => { @@ -1823,6 +1925,7 @@ describe('Authenticator', () => { } ) ); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); }); }); @@ -1831,16 +1934,10 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessVal: SessionValue; - const auditLogger = { - log: jest.fn(), - enabled: true, - }; beforeEach(() => { - auditLogger.log.mockClear(); mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); mockOptions.session.get.mockResolvedValue(null); - mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); authenticator = new Authenticator(mockOptions); @@ -1863,8 +1960,8 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); - expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not redirect to Login Selector even if it is enabled if session is not available.', async () => { @@ -1885,8 +1982,8 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); - expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not clear session if provider cannot handle authentication', async () => { @@ -1905,12 +2002,12 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); - expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.authenticate).toBeCalledWith( request, mockSessVal.state ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('does not clear session if authentication fails with non-401 reason.', async () => { @@ -1930,6 +2027,7 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('extends session if no update is needed.', async () => { @@ -1945,11 +2043,12 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user) ); - expect(mockOptions.session.extend).toHaveBeenCalledTimes(1); - expect(mockOptions.session.extend).toHaveBeenCalledWith(request, mockSessVal); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); + expect(mockOptions.session.extend).toHaveBeenCalledTimes(1); + expect(mockOptions.session.extend).toHaveBeenCalledWith(request, mockSessVal); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('replaces existing session with the one returned by authentication provider', async () => { @@ -1966,14 +2065,15 @@ describe('Authenticator', () => { AuthenticationResult.succeeded(user, { state: newState }) ); + expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).toHaveBeenCalledTimes(1); expect(mockOptions.session.update).toHaveBeenCalledWith(request, { ...mockSessVal, state: newState, }); - expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('clears session if provider failed to authenticate request with 401.', async () => { @@ -1991,18 +2091,12 @@ describe('Authenticator', () => { AuthenticationResult.failed(failureReason) ); - expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); expect(mockOptions.session.create).not.toHaveBeenCalled(); expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.session.extend).not.toHaveBeenCalled(); - - expect(auditLogger.log).toHaveBeenCalledTimes(1); - expect(auditLogger.log).toHaveBeenCalledWith( - expect.objectContaining({ - event: { action: 'user_logout', category: ['authentication'], outcome: 'unknown' }, - }) - ); + expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' }); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); }); @@ -2010,15 +2104,9 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessVal: SessionValue; - const auditLogger = { - log: jest.fn(), - enabled: true, - }; beforeEach(() => { - auditLogger.log.mockClear(); mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); - mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); authenticator = new Authenticator(mockOptions); @@ -2028,6 +2116,7 @@ describe('Authenticator', () => { await expect(authenticator.logout(undefined as any)).rejects.toThrowError( 'Request should be a valid "KibanaRequest" instance, was [undefined].' ); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('redirects to login form if session does not exist.', async () => { @@ -2040,6 +2129,7 @@ describe('Authenticator', () => { ); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('clears session and returns whatever authentication provider returns.', async () => { @@ -2055,25 +2145,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockOptions.session.invalidate).toHaveBeenCalled(); - }); - - it('adds audit event.', async () => { - const request = httpServerMock.createKibanaRequest(); - mockBasicAuthenticationProvider.logout.mockResolvedValue( - DeauthenticationResult.redirectTo('some-url') - ); - mockOptions.session.get.mockResolvedValue(mockSessVal); - - await expect(authenticator.logout(request)).resolves.toEqual( - DeauthenticationResult.redirectTo('some-url') - ); - - expect(auditLogger.log).toHaveBeenCalledTimes(1); - expect(auditLogger.log).toHaveBeenCalledWith( - expect.objectContaining({ - event: { action: 'user_logout', category: ['authentication'], outcome: 'unknown' }, - }) - ); + expectAuditEvents({ action: 'user_logout', outcome: 'unknown' }); }); it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => { @@ -2093,6 +2165,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null); expect(mockOptions.session.invalidate).toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => { @@ -2110,6 +2183,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1); expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('if session does not exist and providers is empty, redirects to default logout path.', async () => { @@ -2128,6 +2202,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('redirects to login form if session does not exist and provider name is invalid', async () => { @@ -2140,6 +2215,7 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled(); expect(mockOptions.session.invalidate).toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); }); @@ -2147,16 +2223,11 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessionValue: SessionValue; - const auditLogger = { - log: jest.fn(), - enabled: true, - }; beforeEach(() => { mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); mockSessionValue = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); mockOptions.session.get.mockResolvedValue(mockSessionValue); - mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockOptions.getCurrentUser.mockReturnValue(mockAuthenticatedUser()); mockOptions.license.getFeatures.mockReturnValue({ allowAccessAgreement: true, @@ -2176,6 +2247,7 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('fails if cannot retrieve user session', async () => { @@ -2189,6 +2261,7 @@ describe('Authenticator', () => { expect(mockOptions.session.update).not.toHaveBeenCalled(); expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); }); it('fails if license does not allow access agreement acknowledgement', async () => { @@ -2203,6 +2276,7 @@ describe('Authenticator', () => { ); expect(mockOptions.session.update).not.toHaveBeenCalled(); + expect(auditLogger.log).not.toHaveBeenCalled(); expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled(); }); @@ -2215,17 +2289,10 @@ describe('Authenticator', () => { ...mockSessionValue, accessAgreementAcknowledged: true, }); - - expect(auditLogger.log).toHaveBeenCalledTimes(1); - expect(auditLogger.log).toHaveBeenCalledWith( - expect.objectContaining({ - event: { action: 'access_agreement_acknowledged', category: ['authentication'] }, - }) - ); - expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).toHaveBeenCalledTimes( 1 ); + expectAuditEvents({ action: 'access_agreement_acknowledged' }); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index fd1de8af4a149..3cddcd48dda7e 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -90,6 +90,16 @@ export interface AuthenticatorOptions { getServerBaseURL: () => string; } +/** @internal */ +interface InvalidateSessionValueParams { + /** Request instance. */ + request: KibanaRequest; + /** Value of the existing session, if any. */ + sessionValue: SessionValue | null; + /** If enabled, skips writing a `user_logout` audit event for this session. */ + skipAuditEvent?: boolean; +} + // Mapping between provider key defined in the config and authentication // provider class that can handle specific authentication mechanism. const providerMap = new Map< @@ -325,6 +335,9 @@ export class Authenticator { const auditLogger = this.options.audit.asScoped(request); auditLogger.log( userLoginEvent({ + // We must explicitly specify the sessionId for login events because we just created the session, so + // it won't automatically get included in the audit event from the request context. + sessionId: sessionUpdateResult?.value?.sid, authenticationResult, authenticationProvider: providerName, authenticationType: provider.type, @@ -446,7 +459,7 @@ export class Authenticator { sessionValue?.provider.name ?? request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER); if (suggestedProviderName) { - await this.invalidateSessionValue(request, sessionValue); + await this.invalidateSessionValue({ request, sessionValue }); // Provider name may be passed in a query param and sourced from the browser's local storage; // hence, we can't assume that this provider exists, so we have to check it. @@ -595,7 +608,7 @@ export class Authenticator { this.logger.warn( `Attempted to retrieve session for the "${existingSessionValue.provider.type}/${existingSessionValue.provider.name}" provider, but it is not configured.` ); - await this.invalidateSessionValue(request, existingSessionValue); + await this.invalidateSessionValue({ request, sessionValue: existingSessionValue }); return null; } @@ -629,7 +642,7 @@ export class Authenticator { // attempt didn't fail. if (authenticationResult.shouldClearState()) { this.logger.debug('Authentication provider requested to invalidate existing session.'); - await this.invalidateSessionValue(request, existingSessionValue); + await this.invalidateSessionValue({ request, sessionValue: existingSessionValue }); return null; } @@ -643,7 +656,7 @@ export class Authenticator { if (authenticationResult.failed()) { if (ownsSession && getErrorStatusCode(authenticationResult.error) === 401) { this.logger.debug('Authentication attempt failed, existing session will be invalidated.'); - await this.invalidateSessionValue(request, existingSessionValue); + await this.invalidateSessionValue({ request, sessionValue: existingSessionValue }); } return null; } @@ -681,17 +694,21 @@ export class Authenticator { this.logger.debug( 'Authentication provider has changed, existing session will be invalidated.' ); - await this.invalidateSessionValue(request, existingSessionValue); + await this.invalidateSessionValue({ request, sessionValue: existingSessionValue }); existingSessionValue = null; } else if (sessionHasBeenAuthenticated) { this.logger.debug( 'Session is authenticated, existing unauthenticated session will be invalidated.' ); - await this.invalidateSessionValue(request, existingSessionValue); + await this.invalidateSessionValue({ + request, + sessionValue: existingSessionValue, + skipAuditEvent: true, // Skip writing an audit event when we are replacing an intermediate session with a fullly authenticated session + }); existingSessionValue = null; } else if (usernameHasChanged) { this.logger.debug('Username has changed, existing session will be invalidated.'); - await this.invalidateSessionValue(request, existingSessionValue); + await this.invalidateSessionValue({ request, sessionValue: existingSessionValue }); existingSessionValue = null; } @@ -726,11 +743,13 @@ export class Authenticator { /** * Invalidates session value associated with the specified request. - * @param request Request instance. - * @param sessionValue Value of the existing session if any. */ - private async invalidateSessionValue(request: KibanaRequest, sessionValue: SessionValue | null) { - if (sessionValue) { + private async invalidateSessionValue({ + request, + sessionValue, + skipAuditEvent, + }: InvalidateSessionValueParams) { + if (sessionValue && !skipAuditEvent) { const auditLogger = this.options.audit.asScoped(request); auditLogger.log( userLogoutEvent({ diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index 491d6cdafa44d..de484647ffb6d 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -10,7 +10,7 @@ import type { TransportResult } from '@elastic/elasticsearch'; import { licenseMock } from '../common/licensing/index.mock'; import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock'; import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock'; -import { auditServiceMock } from './audit/index.mock'; +import { auditServiceMock } from './audit/mocks'; import { authenticationServiceMock } from './authentication/authentication_service.mock'; import { authorizationMock } from './authorization/index.mock'; diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index d890861849cfe..7e45946c7452d 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -15,10 +15,10 @@ import type { SavedObjectsResolveResponse, SavedObjectsUpdateObjectsSpacesResponseObject, } from 'src/core/server'; -import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; import type { AuditEvent } from '../audit'; -import { auditServiceMock } from '../audit/index.mock'; +import { auditLoggerMock } from '../audit/mocks'; import { Actions } from '../authorization'; import type { SavedObjectActions } from '../authorization/actions/saved_object'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; @@ -65,7 +65,7 @@ const createSecureSavedObjectsClientWrapperOptions = () => { checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(), errors, getSpacesService, - auditLogger: auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()), + auditLogger: auditLoggerMock.create(), forbiddenError, generalError, }; diff --git a/x-pack/plugins/security/server/session_management/session_index.test.ts b/x-pack/plugins/security/server/session_management/session_index.test.ts index 45ce865de5635..1667ae8b400be 100644 --- a/x-pack/plugins/security/server/session_management/session_index.test.ts +++ b/x-pack/plugins/security/server/session_management/session_index.test.ts @@ -18,7 +18,7 @@ import type { ElasticsearchClient } from 'src/core/server'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import type { AuditLogger } from '../audit'; -import { auditServiceMock } from '../audit/index.mock'; +import { auditLoggerMock } from '../audit/mocks'; import { ConfigSchema, createConfig } from '../config'; import { securityMock } from '../mocks'; import { getSessionIndexTemplate, SessionIndex } from './session_index'; @@ -32,7 +32,7 @@ describe('Session index', () => { const indexTemplateName = '.kibana_some_tenant_security_session_index_template_1'; beforeEach(() => { mockElasticsearchClient = elasticsearchServiceMock.createElasticsearchClient(); - auditLogger = auditServiceMock.create().withoutRequest; + auditLogger = auditLoggerMock.create(); sessionIndex = new SessionIndex({ logger: loggingSystemMock.createLogger(), kibanaIndexName: '.kibana_some_tenant', @@ -364,7 +364,7 @@ describe('Session index', () => { { isTLSEnabled: false } ), elasticsearchClient: mockElasticsearchClient, - auditLogger: auditServiceMock.create().withoutRequest, + auditLogger, }); await sessionIndex.cleanUp(); @@ -456,7 +456,7 @@ describe('Session index', () => { { isTLSEnabled: false } ), elasticsearchClient: mockElasticsearchClient, - auditLogger: auditServiceMock.create().withoutRequest, + auditLogger, }); await sessionIndex.cleanUp(); @@ -542,7 +542,7 @@ describe('Session index', () => { { isTLSEnabled: false } ), elasticsearchClient: mockElasticsearchClient, - auditLogger: auditServiceMock.create().withoutRequest, + auditLogger, }); await sessionIndex.cleanUp(); @@ -653,7 +653,7 @@ describe('Session index', () => { { isTLSEnabled: false } ), elasticsearchClient: mockElasticsearchClient, - auditLogger: auditServiceMock.create().withoutRequest, + auditLogger, }); await sessionIndex.cleanUp(); diff --git a/x-pack/plugins/security/server/session_management/session_management_service.test.ts b/x-pack/plugins/security/server/session_management/session_management_service.test.ts index 100d0b30082c6..a045a66f42fc0 100644 --- a/x-pack/plugins/security/server/session_management/session_management_service.test.ts +++ b/x-pack/plugins/security/server/session_management/session_management_service.test.ts @@ -16,7 +16,7 @@ import type { } from '../../../task_manager/server'; import { taskManagerMock } from '../../../task_manager/server/mocks'; import type { AuditLogger } from '../audit'; -import { auditServiceMock } from '../audit/index.mock'; +import { auditLoggerMock } from '../audit/mocks'; import { ConfigSchema, createConfig } from '../config'; import type { OnlineStatusRetryScheduler } from '../elasticsearch'; import { Session } from './session'; @@ -37,7 +37,7 @@ describe('SessionManagementService', () => { let auditLogger: AuditLogger; beforeEach(() => { service = new SessionManagementService(loggingSystemMock.createLogger()); - auditLogger = auditServiceMock.create().withoutRequest; + auditLogger = auditLoggerMock.create(); }); afterEach(() => { diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts index 2e39810d4cbde..607ea032078ee 100644 --- a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts @@ -20,7 +20,7 @@ import type { GetAllSpacesPurpose, LegacyUrlAliasTarget, Space } from '../../../ import { spacesClientMock } from '../../../spaces/server/mocks'; import type { AuditEvent, AuditLogger } from '../audit'; import { SavedObjectAction, SpaceAuditAction } from '../audit'; -import { auditServiceMock } from '../audit/index.mock'; +import { auditLoggerMock } from '../audit/mocks'; import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal, @@ -98,7 +98,7 @@ const setup = ({ securityEnabled = false }: Opts = {}) => { }); authorization.mode.useRbacForRequest.mockReturnValue(securityEnabled); - const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); + const auditLogger = auditLoggerMock.create(); const request = httpServerMock.createKibanaRequest(); diff --git a/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts b/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts index 89f0f81dcc5f9..e151b943e2b04 100644 --- a/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts +++ b/x-pack/plugins/security/server/spaces/setup_spaces_client.test.ts @@ -8,7 +8,7 @@ import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { spacesMock } from '../../../spaces/server/mocks'; -import { auditServiceMock } from '../audit/index.mock'; +import { auditServiceMock } from '../audit/mocks'; import { authorizationMock } from '../authorization/index.mock'; import { setupSpacesClient } from './setup_spaces_client'; diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/post/index.tsx b/x-pack/plugins/security_solution/public/common/components/news_feed/post/index.tsx index 484452e68e3a6..2c4042d57561f 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/post/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/post/index.tsx @@ -32,9 +32,9 @@ export const Post = React.memo<{ newsItem: NewsItem }>(({ newsItem }) => { return ( - - {title} - + + {title} + @@ -45,11 +45,9 @@ export const Post = React.memo<{ newsItem: NewsItem }>(({ newsItem }) => { - {imageUrl && ( - - - - )} +

); diff --git a/x-pack/plugins/security_solution/public/common/experimental_features_service.ts b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts index 813341f175408..bb03fb59bf7a5 100644 --- a/x-pack/plugins/security_solution/public/common/experimental_features_service.ts +++ b/x-pack/plugins/security_solution/public/common/experimental_features_service.ts @@ -24,7 +24,7 @@ export class ExperimentalFeaturesService { private static throwUninitializedError(): never { throw new Error( - 'Experimental features services not initialized - are you trying to import this module from outside of the Security Solution app?' + 'Technical preview features services not initialized - are you trying to import this module from outside of the Security Solution app?' ); } } diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 6d5d2dcbc7b4d..161cc62d6e731 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -62,7 +62,7 @@ export interface AppContextTestRender { render: UiRender; /** - * Set experimental features on/off. Calling this method updates the Store with the new values + * Set technical preview features on/off. Calling this method updates the Store with the new values * for the given feature flags * @param flags */ @@ -70,7 +70,7 @@ export interface AppContextTestRender { } // Defined a private custom reducer that reacts to an action that enables us to updat the -// store with new values for experimental features/flags. Because the `action.type` is a `Symbol`, +// store with new values for technical preview features/flags. Because the `action.type` is a `Symbol`, // and its not exported the action can only be `dispatch`'d from this module const UpdateExperimentalFeaturesTestActionType = Symbol('updateExperimentalFeaturesTestAction'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 2ea37ccfd343b..32745f39d27a8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -24,7 +24,7 @@ export const BACK_TO_RULES = i18n.translate( export const EXPERIMENTAL = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.experimentalDescription', { - defaultMessage: 'Experimental', + defaultMessage: 'Technical preview', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 7972eb90310c1..24d842eb930a8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -55,7 +55,7 @@ export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine export const EXPERIMENTAL_ON = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.experimentalOn', { - defaultMessage: 'Experimental: On', + defaultMessage: 'Technical preview: On', } ); @@ -63,14 +63,14 @@ export const EXPERIMENTAL_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.experimentalDescription', { defaultMessage: - 'The experimental rules table view allows for advanced sorting capabilities. If you experience performance issues when working with the table, you can turn this setting off.', + 'The experimental rules table view is in technical preview and allows for advanced sorting capabilities. If you experience performance issues when working with the table, you can turn this setting off.', } ); export const EXPERIMENTAL_OFF = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.experimentalOff', { - defaultMessage: 'Experimental: Off', + defaultMessage: 'Technical preview: Off', } ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts index 787c26871d869..9168e25edfb37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -99,7 +99,7 @@ describe('get_input_output_index', () => { expect(inputIndex).toEqual(DEFAULT_INDEX_PATTERN); }); - test('Returns a saved object inputIndex default along with experimental features when uebaEnabled=true', async () => { + test('Returns a saved object inputIndex default along with technical preview features when uebaEnabled=true', async () => { servicesMock.savedObjectsClient.get.mockImplementation(async (type: string, id: string) => ({ id, type, diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index 300c9c84993a1..01cb39ac87fa8 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -110,6 +110,7 @@ export const usePivotData = ( getDataGridSchemaFromESFieldType, formatHumanReadableDateTimeSeconds, multiColumnSortFactory, + getNestedOrEscapedVal, useDataGrid, INDEX_STATUS, }, @@ -235,7 +236,12 @@ export const usePivotData = ( }, [indexPatternTitle, JSON.stringify([requestPayload, query, combinedRuntimeMappings])]); if (sortingColumns.length > 0) { - tableItems.sort(multiColumnSortFactory(sortingColumns)); + const sortingColumnsWithTypes = sortingColumns.map((c) => ({ + ...c, + // Since items might contain undefined/null values, we want to accurate find the data type + type: typeof tableItems.find((item) => getNestedOrEscapedVal(item, c.id) !== undefined), + })); + tableItems.sort(multiColumnSortFactory(sortingColumnsWithTypes)); } const pageData = tableItems.slice( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts index c627984e0214f..ca073bc82cbc7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts @@ -89,6 +89,5 @@ export function useTableSettings( direction: sortDirection, }, }; - return { onTableChange, pagination, sorting }; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2d72bd20f7712..1dda671cb7f19 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7162,9 +7162,6 @@ "xpack.apm.transactionDetails.statusCode": "ステータスコード", "xpack.apm.transactionDetails.syncBadgeAsync": "非同期", "xpack.apm.transactionDetails.syncBadgeBlocking": "ブロック", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription": "失敗したトランザクションの相関関係はGAではありません。不具合が発生したら報告してください。", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaLabel": "ベータ", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle": "失敗したトランザクションの相関関係", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "失敗したトランザクションの相関関係", "xpack.apm.transactionDetails.tabs.latencyLabel": "遅延の相関関係", "xpack.apm.transactionDetails.tabs.traceSamplesLabel": "トレースのサンプル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97f6c16210de8..6ed3624cf8e8d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7221,9 +7221,6 @@ "xpack.apm.transactionDetails.statusCode": "状态代码", "xpack.apm.transactionDetails.syncBadgeAsync": "异步", "xpack.apm.transactionDetails.syncBadgeBlocking": "正在阻止", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription": "失败事务相关性不是 GA 版。请通过报告错误来帮助我们。", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaLabel": "公测版", - "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle": "失败事务相关性", "xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "失败事务相关性", "xpack.apm.transactionDetails.tabs.latencyLabel": "延迟相关性", "xpack.apm.transactionDetails.tabs.traceSamplesLabel": "跟踪样例", diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index 6606c9a6aa420..dd85fd094a804 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -28,7 +28,7 @@ export default function ({ getService }) { `/api/maps/mvt/getTile/2/1/1.pbf\ ?geometryFieldName=geo.coordinates\ &index=logstash-*\ -&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw))` +&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw,(field:'@timestamp',format:epoch_millis)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw,'@timestamp'))` ) .set('kbn-xsrf', 'kibana') .responseType('blob') @@ -53,6 +53,7 @@ export default function ({ getService }) { expect(feature.extent).to.be(4096); expect(feature.id).to.be(undefined); expect(feature.properties).to.eql({ + '@timestamp': '1442709961071', _id: 'AU_x3_BsGFA8no6Qjjug', _index: 'logstash-2015.09.20', bytes: 9252, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts index c711dff7b55a0..1f58d4b72cea8 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts @@ -100,6 +100,56 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + describe('alert details invalid alerts', () => { + let caseId: string; + + before(async () => { + caseId = await createCaseWithAlerts(); + await esArchiver.load('x-pack/test/functional/es_archives/cases/signals/hosts_users'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/cases/signals/hosts_users'); + await deleteAllCaseItems(es); + }); + + it('ignores failures from alerts that the index does not exist', async () => { + const theCase = await createCase(supertest, getPostCaseRequest()); + + // add an alert that has an index and id do not exist + await createComment({ supertest, caseId: theCase.id, params: postCommentAlertReq }); + + const metrics = await getCaseMetrics({ + supertest, + caseId: theCase.id, + features: ['alerts.users', 'alerts.hosts'], + }); + + expect(metrics.alerts?.hosts).to.eql({ + total: 0, + values: [], + }); + expect(metrics.alerts?.users).to.eql({ + total: 0, + values: [], + }); + }); + + it('returns the accurate metrics for the alerts that have valid indices', async () => { + await createComment({ supertest, caseId, params: postCommentAlertReq }); + + const metrics = await getCaseMetrics({ + supertest, + caseId, + features: ['alerts.users', 'alerts.hosts', 'alerts.count'], + }); + + expect(metrics.alerts?.hosts?.total).to.be(3); + expect(metrics.alerts?.users?.total).to.be(4); + expect(metrics.alerts?.count).to.be(7); + }); + }); + describe('alert count', () => { afterEach(async () => { await deleteAllCaseItems(es); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml index 9c83569a69cbe..62f605c5828f8 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/experimental/0.1.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: experimental +name: experimental title: experimental integration -description: This is a test package for testing experimental packages +description: This is a test package for testing technical preview packages version: 0.1.0 categories: [] release: experimental diff --git a/x-pack/test/functional/apps/maps/file_upload/shapefile.js b/x-pack/test/functional/apps/maps/file_upload/shapefile.js index 30d3aa1ae3b02..059e32861278a 100644 --- a/x-pack/test/functional/apps/maps/file_upload/shapefile.js +++ b/x-pack/test/functional/apps/maps/file_upload/shapefile.js @@ -14,8 +14,7 @@ export default function ({ getPageObjects, getService }) { const security = getService('security'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/124334 - describe.skip('shapefile upload', () => { + describe('shapefile upload', () => { let indexName = ''; before(async () => { await security.testUser.setRoles([ @@ -41,10 +40,14 @@ export default function ({ getPageObjects, getService }) { const numberOfLayers = await PageObjects.maps.getNumberOfLayers(); expect(numberOfLayers).to.be(2); + // preview text is inconsistent. Skip expect for now + // https://github.com/elastic/kibana/issues/124334 + /* const tooltipText = await PageObjects.maps.getLayerTocTooltipMsg('cb_2018_us_csa_500k'); expect(tooltipText).to.be( 'cb_2018_us_csa_500k\nResults limited to 141 features, 81% of file.' ); + */ }); it('should import shapefile', async () => { diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index a5a0d6d2de6d7..a20962e607af2 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -14,6 +14,7 @@ import { farequoteLuceneSearchTestData, sampleLogTestData, } from './index_test_data'; +import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; export default function ({ getPageObject, getService }: FtrProviderContext) { const headerPage = getPageObject('header'); @@ -220,5 +221,51 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); runTests(sampleLogTestData); }); + + describe('with view in lens action ', function () { + const testData = farequoteDataViewTestData; + // Run tests on full ft_module_sample_logs index. + it(`${testData.suiteTitle} loads the data visualizer selector page`, async () => { + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + }); + + it(`${testData.suiteTitle} loads lens charts`, async () => { + await ml.testExecution.logTestStep( + `${testData.suiteTitle} loads the saved search selection page` + ); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep( + `${testData.suiteTitle} loads the index data visualizer page` + ); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer( + testData.sourceIndexOrSavedSearch + ); + + await ml.testExecution.logTestStep(`${testData.suiteTitle} loads data for full time range`); + await ml.dataVisualizerIndexBased.clickUseFullDataButton( + testData.expected.totalDocCountFormatted + ); + await headerPage.waitUntilLoadingHasFinished(); + + await ml.testExecution.logTestStep('navigate to Lens'); + const lensMetricField = testData.expected.metricFields![0]; + + if (lensMetricField) { + await ml.dataVisualizerTable.assertLensActionShowChart(lensMetricField.fieldName); + await ml.navigation.browserBackTo('dataVisualizerTable'); + } + const lensNonMetricField = testData.expected.nonMetricFields?.find( + (f) => f.type === ML_JOB_FIELD_TYPES.KEYWORD + ); + + if (lensNonMetricField) { + await ml.dataVisualizerTable.assertLensActionShowChart(lensNonMetricField.fieldName); + await ml.navigation.browserBackTo('dataVisualizerTable'); + } + }); + }); }); } diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index 24563fe05f6ff..cf9b1f8fa35a5 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -565,6 +565,17 @@ export function MachineLearningDataVisualizerTableProvider( } } + public async assertLensActionShowChart(fieldName: string) { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.clickWhenNotDisabled( + this.rowSelector(fieldName, 'dataVisualizerActionViewInLensButton') + ); + await testSubjects.existOrFail('lnsVisualizationContainer', { + timeout: 15 * 1000, + }); + }); + } + public async ensureNumRowsPerPage(n: 10 | 25 | 50) { const paginationButton = 'dataVisualizerTable > tablePaginationPopoverButton'; await retry.tryForTime(10000, async () => { diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 4f11f082eb152..c11721453d10f 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -17,7 +17,7 @@ export function MachineLearningNavigationProvider({ const browser = getService('browser'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'header']); return { async navigateToMl() { @@ -264,5 +264,11 @@ export function MachineLearningNavigationProvider({ `Expected the current URL "${currentUrl}" to not include ${expectedUrlPart}` ); }, + + async browserBackTo(backTestSubj: string) { + await browser.goBack(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail(backTestSubj, { timeout: 10 * 1000 }); + }, }; }