diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java index b387ff0e2b..b7ab5e9c0b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java @@ -16,13 +16,14 @@ package org.springframework.boot.autoconfigure.elasticsearch; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Duration; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; @@ -36,6 +37,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; /** * Elasticsearch rest client infrastructure configurations. @@ -43,6 +45,7 @@ import org.springframework.context.annotation.Configuration; * @author Brian Clozel * @author Stephane Nicoll * @author Vedran Pavic + * @author Evgeniy Cheban */ class ElasticsearchRestClientConfigurations { @@ -58,7 +61,7 @@ class ElasticsearchRestClientConfigurations { @Bean RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, ObjectProvider builderCustomizers) { - HttpHost[] hosts = properties.getUris().stream().map(HttpHost::create).toArray(HttpHost[]::new); + HttpHost[] hosts = properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); RestClientBuilder builder = RestClient.builder(hosts); builder.setHttpClientConfigCallback((httpClientBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); @@ -72,6 +75,28 @@ class ElasticsearchRestClientConfigurations { return builder; } + private HttpHost createHttpHost(String uri) { + try { + return createHttpHost(URI.create(uri)); + } + catch (IllegalArgumentException ex) { + return HttpHost.create(uri); + } + } + + private HttpHost createHttpHost(URI uri) { + if (!StringUtils.hasLength(uri.getUserInfo())) { + return HttpHost.create(uri.toString()); + } + try { + return HttpHost.create(new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), + uri.getQuery(), uri.getFragment()).toString()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + } @Configuration(proxyBeanMethods = false) @@ -124,13 +149,7 @@ class ElasticsearchRestClientConfigurations { @Override public void customize(HttpAsyncClientBuilder builder) { - map.from(this.properties::getUsername).whenHasText().to((username) -> { - CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - Credentials credentials = new UsernamePasswordCredentials(this.properties.getUsername(), - this.properties.getPassword()); - credentialsProvider.setCredentials(AuthScope.ANY, credentials); - builder.setDefaultCredentialsProvider(credentialsProvider); - }); + builder.setDefaultCredentialsProvider(new PropertiesCredentialsProvider(this.properties)); } @Override @@ -143,4 +162,47 @@ class ElasticsearchRestClientConfigurations { } + private static class PropertiesCredentialsProvider extends BasicCredentialsProvider { + + PropertiesCredentialsProvider(ElasticsearchRestClientProperties properties) { + if (StringUtils.hasText(properties.getUsername())) { + Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), + properties.getPassword()); + setCredentials(AuthScope.ANY, credentials); + } + properties.getUris().stream().map(this::toUri).filter(this::hasUserInfo) + .forEach(this::addUserInfoCredentials); + } + + private URI toUri(String uri) { + try { + return URI.create(uri); + } + catch (IllegalArgumentException ex) { + return null; + } + } + + private boolean hasUserInfo(URI uri) { + return uri != null && StringUtils.hasLength(uri.getUserInfo()); + } + + private void addUserInfoCredentials(URI uri) { + AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort()); + Credentials credentials = createUserInfoCredentials(uri.getUserInfo()); + setCredentials(authScope, credentials); + } + + private Credentials createUserInfoCredentials(String userInfo) { + int delimiter = userInfo.indexOf(":"); + if (delimiter == -1) { + return new UsernamePasswordCredentials(userInfo, null); + } + String username = userInfo.substring(0, delimiter); + String password = userInfo.substring(delimiter + 1); + return new UsernamePasswordCredentials(username, password); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java index 5fdcb32fb8..09648bf9bc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java @@ -20,10 +20,16 @@ import java.time.Duration; import java.util.HashMap; import java.util.Map; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.Node; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; @@ -47,6 +53,7 @@ import static org.mockito.Mockito.mock; * * @author Brian Clozel * @author Vedran Pavic + * @author Evgeniy Cheban */ @Testcontainers(disabledWithoutDocker = true) class ElasticsearchRestClientAutoConfigurationTests { @@ -166,6 +173,69 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } + @Test + void configureUriWithUsernameOnly() { + this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user@localhost:9200") + .run((context) -> { + RestClient client = context.getBean(RestClient.class); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", + InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isNull(); + }); + }); + } + + @Test + void configureUriWithUsernameAndEmptyPassword() { + this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user:@localhost:9200") + .run((context) -> { + RestClient client = context.getBean(RestClient.class); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", + InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isEmpty(); + }); + }); + } + + @Test + void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.rest.uris=http://user:password@localhost:9200,localhost:9201", + "spring.elasticsearch.rest.username=admin", "spring.elasticsearch.rest.password=admin") + .run((context) -> { + RestClient client = context.getBean(RestClient.class); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200", "http://localhost:9201"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", + InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials uriCredentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9200)); + assertThat(uriCredentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(uriCredentials.getPassword()).isEqualTo("password"); + Credentials defaultCredentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9201)); + assertThat(defaultCredentials.getUserPrincipal().getName()).isEqualTo("admin"); + assertThat(defaultCredentials.getPassword()).isEqualTo("admin"); + }); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomRestClientConfiguration {