This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit acc801ac7a6275a6723183a71079f3f918052269 Author: Antoine DUPRAT <[email protected]> AuthorDate: Mon Oct 28 11:55:24 2019 +0100 JAMES-2905 ES authentication configuration --- .../apache/james/backends/es/ClientProvider.java | 27 ++++- .../backends/es/ElasticSearchConfiguration.java | 122 +++++++++++++++++++- .../es/ElasticSearchConfigurationTest.java | 123 +++++++++++++++++++++ .../modules/mailbox/ElasticSearchStartUpCheck.java | 3 +- 4 files changed, 267 insertions(+), 8 deletions(-) diff --git a/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ClientProvider.java b/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ClientProvider.java index f6deb48..5412b09 100644 --- a/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ClientProvider.java +++ b/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ClientProvider.java @@ -21,6 +21,7 @@ package org.apache.james.backends.es; import java.io.IOException; import java.time.Duration; import java.time.LocalDateTime; +import java.util.Optional; import javax.annotation.PreDestroy; import javax.inject.Inject; @@ -28,7 +29,12 @@ import javax.inject.Provider; import org.apache.commons.lang3.time.DurationFormatUtils; import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +47,6 @@ public class ClientProvider implements Provider<RestHighLevelClient> { private static final Logger LOGGER = LoggerFactory.getLogger(ClientProvider.class); - private static final String HTTP_HOST_SCHEME = "http"; private final ElasticSearchConfiguration configuration; private final RestHighLevelClient client; @@ -67,14 +72,30 @@ public class ClientProvider implements Provider<RestHighLevelClient> { private RestHighLevelClient connectToCluster(ElasticSearchConfiguration configuration) throws IOException { LOGGER.info("Trying to connect to ElasticSearch service at {}", LocalDateTime.now()); + Optional<CredentialsProvider> credentials = credentials(configuration); + RestClientBuilder restClientBuilder = RestClient.builder(hostsToHttpHosts()); + credentials.ifPresent(provider -> restClientBuilder + .setHttpClientConfigCallback(httpClientBuilder -> { + return httpClientBuilder.setDefaultCredentialsProvider(provider); + })); + return new RestHighLevelClient( - RestClient.builder(hostsToHttpHosts()) + restClientBuilder .setMaxRetryTimeoutMillis(Math.toIntExact(configuration.getRequestTimeout().toMillis()))); } + private Optional<CredentialsProvider> credentials(ElasticSearchConfiguration configuration) { + if (configuration.getUser().isPresent() && configuration.getPassword().isPresent()) { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(configuration.getUser().get(), configuration.getPassword().get())); + return Optional.of(credentialsProvider); + } + return Optional.empty(); + } + private HttpHost[] hostsToHttpHosts() { return configuration.getHosts().stream() - .map(host -> new HttpHost(host.getHostName(), host.getPort(), HTTP_HOST_SCHEME)) + .map(host -> new HttpHost(host.getHostName(), host.getPort(), configuration.getHostScheme().name())) .toArray(HttpHost[]::new); } diff --git a/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ElasticSearchConfiguration.java b/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ElasticSearchConfiguration.java index f50200f..875bf01 100644 --- a/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ElasticSearchConfiguration.java +++ b/backends-common/elasticsearch/src/main/java/org/apache/james/backends/es/ElasticSearchConfiguration.java @@ -37,6 +37,67 @@ import com.google.common.collect.ImmutableList; public class ElasticSearchConfiguration { + public enum HostScheme { + HTTP("http"), + HTTPS("https"); + + public static HostScheme of(String schemeValue) { + return Arrays.stream(values()) + .filter(hostScheme -> hostScheme.value.equalsIgnoreCase(schemeValue)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + String.format("Unknown HostScheme '%s'", schemeValue))); + } + + private final String value; + + HostScheme(String value) { + this.value = value; + } + } + + public static class Credential { + + public static Credential of(String username, String password) { + return new Credential(username, password); + } + + private final String username; + private final String password; + + private Credential(String username, String password) { + Preconditions.checkNotNull(username, "username cannot be null when password is specified"); + Preconditions.checkNotNull(password, "password cannot be null when username is specified"); + + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof Credential) { + Credential that = (Credential) o; + + return Objects.equals(this.username, that.username) + && Objects.equals(this.password, that.password); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(username, password); + } + } + public static class Builder { private final ImmutableList.Builder<Host> hosts; @@ -46,6 +107,8 @@ public class ElasticSearchConfiguration { private Optional<Integer> minDelay; private Optional<Integer> maxRetries; private Optional<Duration> requestTimeout; + private Optional<HostScheme> hostScheme; + private Optional<Credential> credential; public Builder() { hosts = ImmutableList.builder(); @@ -55,6 +118,8 @@ public class ElasticSearchConfiguration { minDelay = Optional.empty(); maxRetries = Optional.empty(); requestTimeout = Optional.empty(); + hostScheme = Optional.empty(); + credential = Optional.empty(); } public Builder addHost(Host host) { @@ -100,6 +165,16 @@ public class ElasticSearchConfiguration { return this; } + public Builder hostScheme(Optional<HostScheme> hostScheme) { + this.hostScheme = hostScheme; + return this; + } + + public Builder credential(Optional<Credential> credential) { + this.credential = credential; + return this; + } + public ElasticSearchConfiguration build() { ImmutableList<Host> hosts = this.hosts.build(); Preconditions.checkState(!hosts.isEmpty(), "You need to specify ElasticSearch host"); @@ -110,7 +185,9 @@ public class ElasticSearchConfiguration { waitForActiveShards.orElse(DEFAULT_WAIT_FOR_ACTIVE_SHARDS), minDelay.orElse(DEFAULT_CONNECTION_MIN_DELAY), maxRetries.orElse(DEFAULT_CONNECTION_MAX_RETRIES), - requestTimeout.orElse(DEFAULT_REQUEST_TIMEOUT)); + requestTimeout.orElse(DEFAULT_REQUEST_TIMEOUT), + hostScheme.orElse(DEFAULT_SCHEME), + credential); } } @@ -121,6 +198,9 @@ public class ElasticSearchConfiguration { public static final String ELASTICSEARCH_HOSTS = "elasticsearch.hosts"; public static final String ELASTICSEARCH_MASTER_HOST = "elasticsearch.masterHost"; public static final String ELASTICSEARCH_PORT = "elasticsearch.port"; + public static final String ELASTICSEARCH_HOST_SCHEME = "elasticsearch.hostScheme"; + public static final String ELASTICSEARCH_USER = "elasticsearch.user"; + public static final String ELASTICSEARCH_PASSWORD = "elasticsearch.password"; public static final String ELASTICSEARCH_NB_REPLICA = "elasticsearch.nb.replica"; public static final String WAIT_FOR_ACTIVE_SHARDS = "elasticsearch.index.waitForActiveShards"; public static final String ELASTICSEARCH_NB_SHARDS = "elasticsearch.nb.shards"; @@ -136,6 +216,7 @@ public class ElasticSearchConfiguration { public static final int DEFAULT_PORT = 9200; public static final String LOCALHOST = "127.0.0.1"; public static final Optional<Integer> DEFAULT_PORT_AS_OPTIONAL = Optional.of(DEFAULT_PORT); + public static final HostScheme DEFAULT_SCHEME = HostScheme.HTTP; public static final ElasticSearchConfiguration DEFAULT_CONFIGURATION = builder() .addHost(Host.from(LOCALHOST, DEFAULT_PORT)) @@ -144,6 +225,8 @@ public class ElasticSearchConfiguration { public static ElasticSearchConfiguration fromProperties(Configuration configuration) throws ConfigurationException { return builder() .addHosts(getHosts(configuration)) + .hostScheme(getHostScheme(configuration)) + .credential(getCredential(configuration)) .nbShards(configuration.getInteger(ELASTICSEARCH_NB_SHARDS, DEFAULT_NB_SHARDS)) .nbReplica(configuration.getInteger(ELASTICSEARCH_NB_REPLICA, DEFAULT_NB_REPLICA)) .waitForActiveShards(configuration.getInteger(WAIT_FOR_ACTIVE_SHARDS, DEFAULT_WAIT_FOR_ACTIVE_SHARDS)) @@ -152,6 +235,22 @@ public class ElasticSearchConfiguration { .build(); } + private static Optional<HostScheme> getHostScheme(Configuration configuration) { + return Optional.ofNullable(configuration.getString(ELASTICSEARCH_HOST_SCHEME)) + .map(HostScheme::of); + } + + private static Optional<Credential> getCredential(Configuration configuration) { + String username = configuration.getString(ELASTICSEARCH_USER); + String password = configuration.getString(ELASTICSEARCH_PASSWORD); + + if (username == null && password == null) { + return Optional.empty(); + } + + return Optional.of(Credential.of(username, password)); + } + private static ImmutableList<Host> getHosts(Configuration propertiesReader) throws ConfigurationException { Optional<String> masterHost = Optional.ofNullable( propertiesReader.getString(ELASTICSEARCH_MASTER_HOST, null)); @@ -194,8 +293,11 @@ public class ElasticSearchConfiguration { private final int minDelay; private final int maxRetries; private final Duration requestTimeout; + private final HostScheme hostScheme; + private final Optional<Credential> credential; - private ElasticSearchConfiguration(ImmutableList<Host> hosts, int nbShards, int nbReplica, int waitForActiveShards, int minDelay, int maxRetries, Duration requestTimeout) { + private ElasticSearchConfiguration(ImmutableList<Host> hosts, int nbShards, int nbReplica, int waitForActiveShards, int minDelay, int maxRetries, Duration requestTimeout, + HostScheme hostScheme, Optional<Credential> credential) { this.hosts = hosts; this.nbShards = nbShards; this.nbReplica = nbReplica; @@ -203,6 +305,8 @@ public class ElasticSearchConfiguration { this.minDelay = minDelay; this.maxRetries = maxRetries; this.requestTimeout = requestTimeout; + this.hostScheme = hostScheme; + this.credential = credential; } public ImmutableList<Host> getHosts() { @@ -233,6 +337,14 @@ public class ElasticSearchConfiguration { return requestTimeout; } + public HostScheme getHostScheme() { + return hostScheme; + } + + public Optional<Credential> getCredential() { + return credential; + } + @Override public final boolean equals(Object o) { if (o instanceof ElasticSearchConfiguration) { @@ -244,13 +356,15 @@ public class ElasticSearchConfiguration { && Objects.equals(this.minDelay, that.minDelay) && Objects.equals(this.maxRetries, that.maxRetries) && Objects.equals(this.hosts, that.hosts) - && Objects.equals(this.requestTimeout, that.requestTimeout); + && Objects.equals(this.requestTimeout, that.requestTimeout) + && Objects.equals(this.hostScheme, that.hostScheme) + && Objects.equals(this.credential, that.credential); } return false; } @Override public final int hashCode() { - return Objects.hash(hosts, nbShards, nbReplica, waitForActiveShards, minDelay, maxRetries, requestTimeout); + return Objects.hash(hosts, nbShards, nbReplica, waitForActiveShards, minDelay, maxRetries, requestTimeout, hostScheme, credential); } } diff --git a/backends-common/elasticsearch/src/test/java/org/apache/james/backends/es/ElasticSearchConfigurationTest.java b/backends-common/elasticsearch/src/test/java/org/apache/james/backends/es/ElasticSearchConfigurationTest.java index befa774..5013204 100644 --- a/backends-common/elasticsearch/src/test/java/org/apache/james/backends/es/ElasticSearchConfigurationTest.java +++ b/backends-common/elasticsearch/src/test/java/org/apache/james/backends/es/ElasticSearchConfigurationTest.java @@ -27,7 +27,10 @@ import java.util.Optional; import org.apache.commons.configuration2.PropertiesConfiguration; import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler; import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.james.backends.es.ElasticSearchConfiguration.Credential; +import org.apache.james.backends.es.ElasticSearchConfiguration.HostScheme; import org.apache.james.util.Host; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import com.google.common.collect.ImmutableList; @@ -36,6 +39,26 @@ import nl.jqno.equalsverifier.EqualsVerifier; class ElasticSearchConfigurationTest { + @Nested + class HostSchemeTest { + + @Test + void shouldMatchBeanContact() { + EqualsVerifier.forClass(HostScheme.class) + .verify(); + } + } + + @Nested + class CredentialTest { + + @Test + void shouldMatchBeanContact() { + EqualsVerifier.forClass(Credential.class) + .verify(); + } + } + @Test void elasticSearchConfigurationShouldRespectBeanContract() { EqualsVerifier.forClass(ElasticSearchConfiguration.class) @@ -326,4 +349,104 @@ class ElasticSearchConfigurationTest { .nbShards(0)) .isInstanceOf(IllegalArgumentException.class); } + + @Test + void getCredentialShouldReturnConfiguredValue() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + String user = "johndoe"; + String password = "secret"; + configuration.addProperty("elasticsearch.user", user); + configuration.addProperty("elasticsearch.password", password); + + ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getCredential()) + .contains(Credential.of(user, password)); + } + + @Test + void getCredentialShouldReturnEmptyWhenNotConfigured() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getCredential()) + .isEmpty(); + } + + @Test + void fromPropertiesShouldThrowWhenOnlyUsername() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + configuration.addProperty("elasticsearch.user", "username"); + + assertThatThrownBy(() -> ElasticSearchConfiguration.fromProperties(configuration)) + .isInstanceOf(NullPointerException.class) + .hasMessage("password cannot be null when username is specified"); + } + + @Test + void fromPropertiesShouldThrowWhenOnlyPassword() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + configuration.addProperty("elasticsearch.password", "password"); + + assertThatThrownBy(() -> ElasticSearchConfiguration.fromProperties(configuration)) + .isInstanceOf(NullPointerException.class) + .hasMessage("username cannot be null when password is specified"); + } + + @Test + void getHostSchemeShouldReturnConfiguredValue() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + configuration.addProperty("elasticsearch.hostScheme", "https"); + + ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getHostScheme()) + .isEqualTo(HostScheme.HTTPS); + } + + @Test + void getHostSchemeShouldBeCaseInsensitive() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + configuration.addProperty("elasticsearch.hostScheme", "HTTPs"); + + ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getHostScheme()) + .isEqualTo(HostScheme.HTTPS); + } + + @Test + void getHostSchemeShouldReturnHttpWhenNotConfigured() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + ElasticSearchConfiguration elasticSearchConfiguration = ElasticSearchConfiguration.fromProperties(configuration); + + assertThat(elasticSearchConfiguration.getHostScheme()) + .isEqualTo(HostScheme.HTTP); + } + + @Test + void fromPropertiesShouldThrowWhenInvalidValue() throws Exception { + PropertiesConfiguration configuration = new PropertiesConfiguration(); + configuration.addProperty("elasticsearch.hosts", "127.0.0.1"); + + configuration.addProperty("elasticsearch.hostScheme", "invalid-protocol"); + + assertThatThrownBy(() -> ElasticSearchConfiguration.fromProperties(configuration)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown HostScheme 'invalid-protocol'"); + } } diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/ElasticSearchStartUpCheck.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/ElasticSearchStartUpCheck.java index e8eb258..4bf89d7 100644 --- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/ElasticSearchStartUpCheck.java +++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/ElasticSearchStartUpCheck.java @@ -26,6 +26,7 @@ import javax.inject.Inject; import org.apache.james.backends.es.ElasticSearchConfiguration; import org.apache.james.lifecycle.api.StartUpCheck; import org.elasticsearch.Version; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +50,7 @@ public class ElasticSearchStartUpCheck implements StartUpCheck { @Override public CheckResult check() { try { - Version esVersion = client.info() + Version esVersion = client.info(RequestOptions.DEFAULT) .getVersion(); if (esVersion.isCompatible(RECOMMENDED_ES_VERSION)) { return CheckResult.builder() --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
