diff --git a/docs/changelog/130158.yaml b/docs/changelog/130158.yaml new file mode 100644 index 0000000000000..ffacbcfc740ef --- /dev/null +++ b/docs/changelog/130158.yaml @@ -0,0 +1,5 @@ +pr: 130158 +summary: Handle unavailable MD5 in ES|QL +area: ES|QL +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/common/util/Result.java b/server/src/main/java/org/elasticsearch/common/util/Result.java new file mode 100644 index 0000000000000..57bcb56861da9 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/util/Result.java @@ -0,0 +1,102 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.common.util; + +import org.elasticsearch.common.CheckedSupplier; + +import java.util.Optional; + +/** + * A wrapper around either + * + */ +public abstract class Result implements CheckedSupplier { + + public static Result of(V value) { + return new Success<>(value); + } + + public static Result failure(E exception) { + return new Failure<>(exception); + } + + private Result() {} + + public abstract V get() throws E; + + public abstract Optional failure(); + + public abstract boolean isSuccessful(); + + public boolean isFailure() { + return isSuccessful() == false; + }; + + public abstract Optional asOptional(); + + private static class Success extends Result { + private final V value; + + Success(V value) { + this.value = value; + } + + @Override + public V get() throws E { + return value; + } + + @Override + public Optional failure() { + return Optional.empty(); + } + + @Override + public boolean isSuccessful() { + return true; + } + + @Override + public Optional asOptional() { + return Optional.of(value); + } + } + + private static class Failure extends Result { + private final E exception; + + Failure(E exception) { + this.exception = exception; + } + + @Override + public V get() throws E { + throw exception; + } + + @Override + public Optional failure() { + return Optional.of(exception); + } + + @Override + public boolean isSuccessful() { + return false; + } + + @Override + public Optional asOptional() { + return Optional.empty(); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/util/ResultTests.java b/server/src/test/java/org/elasticsearch/common/util/ResultTests.java new file mode 100644 index 0000000000000..cfb489b6224c6 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/util/ResultTests.java @@ -0,0 +1,47 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.common.util; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty; +import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +public class ResultTests extends ESTestCase { + + public void testSuccess() { + final String str = randomAlphaOfLengthBetween(3, 8); + final Result result = Result.of(str); + assertThat(result.isSuccessful(), is(true)); + assertThat(result.isFailure(), is(false)); + assertThat(result.get(), sameInstance(str)); + assertThat(result.failure(), isEmpty()); + assertThat(result.asOptional(), isPresentWith(str)); + } + + public void testFailure() { + final ElasticsearchException exception = new ElasticsearchStatusException( + randomAlphaOfLengthBetween(10, 30), + RestStatus.INTERNAL_SERVER_ERROR + ); + final Result result = Result.failure(exception); + assertThat(result.isSuccessful(), is(false)); + assertThat(result.isFailure(), is(true)); + assertThat(expectThrows(Exception.class, result::get), sameInstance(exception)); + assertThat(result.failure(), isPresentWith(sameInstance(exception))); + assertThat(result.asOptional(), isEmpty()); + } + +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java index be0a7b2fe27b2..c95e229b04419 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.util.Result; import org.elasticsearch.compute.ann.Evaluator; import org.elasticsearch.compute.ann.Fixed; import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; @@ -202,6 +203,14 @@ public static HashFunction create(BytesRef literal) throws NoSuchAlgorithmExcept return new HashFunction(algorithm, MessageDigest.getInstance(algorithm)); } + public static Result tryCreate(String algorithm) { + try { + return Result.of(new HashFunction(algorithm, MessageDigest.getInstance(algorithm))); + } catch (NoSuchAlgorithmException e) { + return Result.failure(e); + } + } + public HashFunction copy() { try { return new HashFunction(algorithm, MessageDigest.getInstance(algorithm)); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java index b42ec1036cb5b..4d30f6b1b37f4 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java @@ -9,6 +9,8 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.util.Result; +import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -18,17 +20,24 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.string.Hash.HashFunction; import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.List; public class Md5 extends AbstractHashFunction { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "MD5", Md5::new); - private static final HashFunction MD5 = HashFunction.create("MD5"); + /** + * As of Java 14, it is permissible for a JRE to ship without the {@code MD5} {@link MessageDigest}. + * We want the "md5" function in ES|QL to fail at runtime on such platforms (rather than at startup) + * so we wrap the {@link HashFunction} in a {@link Result}. + */ + private static final Result MD5 = HashFunction.tryCreate("MD5"); @FunctionInfo( returnType = "keyword", - description = "Computes the MD5 hash of the input.", + description = "Computes the MD5 hash of the input (if the MD5 hash is available on the JVM).", examples = { @Example(file = "hash", tag = "md5") } ) public Md5(Source source, @Param(name = "input", type = { "keyword", "text" }, description = "Input to hash.") Expression input) { @@ -41,7 +50,12 @@ private Md5(StreamInput in) throws IOException { @Override protected HashFunction getHashFunction() { - return MD5; + try { + return MD5.get(); + } catch (NoSuchAlgorithmException e) { + // Throw a new exception so that the stack trace reflects this call (rather than the static initializer for the MD5 field) + throw new VerificationException("function 'md5' is not available on this platform: {}", e.getMessage()); + } } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java index 871bec7c06804..61577ee56777e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java @@ -13,22 +13,30 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.util.Result; import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.hamcrest.OptionalMatchers; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.junit.After; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.elasticsearch.test.TestMatchers.throwableWithMessage; import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.evaluator; import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.field; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; public class HashStaticTests extends ESTestCase { @@ -45,6 +53,27 @@ public void testInvalidAlgorithmLiteral() { assertThat(e.getMessage(), startsWith("invalid algorithm for [hast(\"invalid\", input)]: invalid MessageDigest not available")); } + public void testTryCreateUnavailableMd5() throws NoSuchAlgorithmException { + assumeFalse("We run with different security providers in FIPS, and changing them at runtime is more complicated", inFipsJvm()); + final Provider sunProvider = Security.getProvider("SUN"); + try { + Security.removeProvider("SUN"); + final Result result = Hash.HashFunction.tryCreate("MD5"); + assertThat(result.isSuccessful(), is(false)); + assertThat(result.failure(), OptionalMatchers.isPresentWith(throwableWithMessage(containsString("MD5")))); + expectThrows(NoSuchAlgorithmException.class, result::get); + } finally { + Security.addProvider(sunProvider); + } + + { + final Result result = Hash.HashFunction.tryCreate("MD5"); + assertThat(result.isSuccessful(), is(true)); + assertThat(result.failure(), OptionalMatchers.isEmpty()); + assertThat(result.get().algorithm(), is("MD5")); + } + } + /** * The following fields and methods were borrowed from AbstractScalarFunctionTestCase */