You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2019/11/13 03:07:05 UTC

[james-project] 16/21: JAMES-2905 ES authentication configuration

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 <ad...@linagora.com>
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: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org