You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by am...@apache.org on 2023/02/17 10:27:30 UTC

[ignite-3] branch main updated: IGNITE-18790 Add SSL support for JDBC driver (#1680)

This is an automated email from the ASF dual-hosted git repository.

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new be6c8b2908 IGNITE-18790 Add SSL support for JDBC driver (#1680)
be6c8b2908 is described below

commit be6c8b290894dbd6f88eaaa2a2aafc3eff300855
Author: Aleksandr <Sa...@icloud.com>
AuthorDate: Fri Feb 17 14:27:25 2023 +0400

    IGNITE-18790 Add SSL support for JDBC driver (#1680)
---
 .../ignite/internal/jdbc/ConnectionProperties.java | 113 +++++++++++
 .../internal/jdbc/ConnectionPropertiesImpl.java    | 206 ++++++++++++++++++++-
 .../ignite/internal/jdbc/JdbcConnection.java       |  22 ++-
 modules/runner/build.gradle                        |   1 +
 .../org/apache/ignite/internal/ssl/ItSslTest.java  | 125 ++++++++++---
 5 files changed, 443 insertions(+), 24 deletions(-)

diff --git a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
index f14547d9ba..ce29e5e572 100644
--- a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
+++ b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionProperties.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.jdbc;
 
 import java.sql.SQLException;
+import org.apache.ignite.client.ClientAuthenticationMode;
 import org.apache.ignite.internal.client.HostAndPortRange;
 
 /**
@@ -126,4 +127,116 @@ public interface ConnectionProperties {
      * @throws SQLException On error.
      */
     void setConnectionTimeout(Integer connTimeout) throws SQLException;
+
+    /**
+     * SSL enabled.
+     *
+     * @return true if SSL is enabled.
+     */
+    boolean isSslEnabled();
+
+    /**
+     * Enable/disable ssl.
+     *
+     * @param enabled true if SSL is enabled.
+     */
+    void setSslEnabled(boolean enabled);
+
+    /**
+     * SSL client authentication. Can be NONE, REQUIRE, and OPTIONAL.
+     *
+     * @param clientAuth SSL client authentication.
+     */
+    void setClientAuth(ClientAuthenticationMode clientAuth);
+
+    /**
+     * SSL client authentication.
+     *
+     * @return SSL client authentication.
+     */
+    ClientAuthenticationMode getClientAuth();
+
+    /**
+     * Set trust store path that will be used to setup SSL connection.
+     *
+     * @param trustStorePath Trust store path.
+     */
+    void setTrustStorePath(String trustStorePath);
+
+    /**
+     * Set trust store password.
+     *
+     * @param password Trust store password.
+     */
+    void setTrustStorePassword(String password);
+
+    /**
+     * Set keystore type. For example, PKSC12 or JKS.
+     *
+     * @param type Truststore type.
+     */
+    void setTrustStoreType(String type);
+
+    /**
+     * Trust store path.
+     *
+     * @return Truststore path.
+     */
+    String getTrustStorePath();
+
+    /**
+     * Truststore password.
+     *
+     * @return Truststore password.
+     */
+    String getTrustStorePassword();
+
+    /**
+     * Truststore type.
+     *
+     * @return Truststore type.
+     */
+    String getTrustStoreType();
+
+    /**
+     * Set keystore type. For example, PKSC12 or JKS.
+     *
+     * @param type Keystore type.
+     */
+    void setKeyStoreType(String type);
+
+    /**
+     * Set key store path that will be used to setup SSL connection.
+     *
+     * @param keyStorePath Key store path.
+     */
+    void setKeyStorePath(String keyStorePath);
+
+    /**
+     * Set key store password.
+     *
+     * @param password Key store password.
+     */
+    void setKeyStorePassword(String password);
+
+    /**
+     * Key store path.
+     *
+     * @return Keystore path.
+     */
+    String getKeyStorePath();
+
+    /**
+     * Keystore password.
+     *
+     * @return Keystore password.
+     */
+    String getKeyStorePassword();
+
+    /**
+     * Keystore type.
+     *
+     * @return Keytore type.
+     */
+    String getKeyStoreType();
 }
diff --git a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
index f3b5c03344..385008f74f 100644
--- a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
+++ b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/ConnectionPropertiesImpl.java
@@ -23,11 +23,14 @@ import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.Properties;
 import java.util.StringTokenizer;
+import java.util.stream.Collectors;
+import org.apache.ignite.client.ClientAuthenticationMode;
 import org.apache.ignite.client.IgniteClientConfiguration;
 import org.apache.ignite.internal.client.HostAndPortRange;
 import org.apache.ignite.internal.jdbc.proto.SqlStateCode;
 import org.apache.ignite.internal.util.ArrayUtils;
 import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -78,8 +81,52 @@ public class ConnectionPropertiesImpl implements ConnectionProperties, Serializa
             "Sets the reconnect throttling retries. Zero means there is no limits.",
             IgniteClientConfiguration.DFLT_RECONNECT_THROTTLING_RETRIES, false, 0, Integer.MAX_VALUE);
 
+    /** Path to the truststore. */
+    private final StringProperty trustStorePath = new StringProperty("trustStorePath",
+            "Path to trust store", null, null, false, null);
+
+    /** Truststore password. */
+    private final StringProperty trustStorePassword = new StringProperty("trustStorePassword",
+            "Trust store password", null, null, false, null);
+
+    /** Type of the truststore. */
+    private final StringProperty trustStoreType = new StringProperty("trustStoreType",
+            "Type of the trust store", "PKCS12", null, false, null);
+
+    /** Path to the keystore. */
+    private final StringProperty keyStorePath = new StringProperty("keyStorePath",
+            "Path to key store", null, null, false, null);
+
+    /** Keystore password. */
+    private final StringProperty keyStorePassword = new StringProperty("keyStorePassword",
+            "Key store password", null, null, false, null);
+
+    /** Type of the keystore. */
+    private final StringProperty keyStoreType = new StringProperty("keyStoreType",
+            "Type of the key store", "PKCS12", null, false, null);
+
+    /** SSL client authentication. */
+    private final StringProperty clientAuth = new StringProperty("clientAuth",
+            "SSL client authentication", "none", clientAuthValues(), false, null);
+
+    @NotNull
+    private static String[] clientAuthValues() {
+        return Arrays.stream(ClientAuthenticationMode.values())
+                .map(Enum::name)
+                .map(String::toLowerCase)
+                .collect(Collectors.toList())
+                .toArray(String[]::new);
+    }
+
+    /** Enable SSL. */
+    private final BooleanProperty sslEnabled = new BooleanProperty("sslEnabled",
+            "Enable ssl", false, null, false, null);
+
     /** Properties array. */
-    private final ConnectionProperty[] propsArray = {qryTimeout, connTimeout};
+    private final ConnectionProperty[] propsArray = {
+            qryTimeout, connTimeout, trustStorePath, trustStorePassword, trustStoreType,
+            sslEnabled, clientAuth, keyStorePath, keyStorePassword, keyStoreType
+    };
 
     /** {@inheritDoc} */
     @Override
@@ -191,6 +238,102 @@ public class ConnectionPropertiesImpl implements ConnectionProperties, Serializa
         connTimeout.setValue(timeout);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public void setTrustStorePath(String trustStorePath) {
+        this.trustStorePath.setValue(trustStorePath);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setTrustStorePassword(String password) {
+        this.trustStorePassword.setValue(password);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getTrustStorePath() {
+        return trustStorePath.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getTrustStorePassword() {
+        return trustStorePassword.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getTrustStoreType() {
+        return trustStoreType.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setKeyStoreType(String type) {
+        keyStoreType.setValue(type);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setKeyStorePath(String keyStorePath) {
+        this.keyStorePath.setValue(keyStorePath);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setKeyStorePassword(String password) {
+        this.keyStorePassword.setValue(password);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getKeyStorePath() {
+        return keyStorePath.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getKeyStorePassword() {
+        return keyStorePassword.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String getKeyStoreType() {
+        return keyStoreType.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setTrustStoreType(String type) {
+        trustStoreType.setValue(type);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isSslEnabled() {
+        return sslEnabled.value();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setSslEnabled(boolean enabled) {
+        sslEnabled.setValue(enabled);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setClientAuth(ClientAuthenticationMode clientAuth) {
+        this.clientAuth.setValue(clientAuth.name().toLowerCase());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ClientAuthenticationMode getClientAuth() {
+        return ClientAuthenticationMode.valueOf(this.clientAuth.value().toUpperCase());
+    }
+
     /**
      * Init connection properties.
      *
@@ -835,6 +978,67 @@ public class ConnectionPropertiesImpl implements ConnectionProperties, Serializa
         }
     }
 
+    /**
+     * Boolean property.
+     */
+    private static class BooleanProperty extends ConnectionProperty {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /** Value. */
+        private boolean val;
+
+        /**
+         * Constructor.
+         *
+         * @param name      Name.
+         * @param desc      Description.
+         * @param dfltVal   Default value.
+         * @param required  {@code true} if the property is required.
+         * @param validator Property value validator.
+         */
+        BooleanProperty(String name, String desc, boolean dfltVal, String[] choices, boolean required,
+                PropertyValidator validator) {
+            super(name, desc, dfltVal, choices, required, validator);
+
+            val = dfltVal;
+        }
+
+        /**
+         * Set the property value.
+         *
+         * @param val Property value.
+         */
+        void setValue(boolean val) {
+            this.val = val;
+        }
+
+        /**
+         * Get the property value.
+         *
+         * @return Property value.
+         */
+        boolean value() {
+            return val;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        void init(String str) throws SQLException {
+            if (validator != null) {
+                validator.validate(str);
+            }
+
+            val = Boolean.parseBoolean(str);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        String valueObject() {
+            return String.valueOf(val);
+        }
+    }
+
     /**
      * Get the driver properties.
      *
diff --git a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
index 724debb087..433f1d9dae 100644
--- a/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
+++ b/modules/jdbc/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
@@ -51,6 +51,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.client.SslConfiguration;
 import org.apache.ignite.internal.client.HostAndPortRange;
 import org.apache.ignite.internal.client.TcpIgniteClient;
 import org.apache.ignite.internal.jdbc.proto.JdbcQueryEventHandler;
@@ -149,12 +150,12 @@ public class JdbcConnection implements Connection {
         int reconnectThrottlingRetries = connProps.getReconnectThrottlingRetries();
 
         try {
-            client = ((TcpIgniteClient) IgniteClient
-                    .builder()
+            client = ((TcpIgniteClient) IgniteClient.builder()
                     .addresses(addrs)
                     .connectTimeout(netTimeout)
                     .reconnectThrottlingPeriod(reconnectThrottlingPeriod)
                     .reconnectThrottlingRetries(reconnectThrottlingRetries)
+                    .ssl(extractSslConfiguration(connProps))
                     .build());
 
         } catch (Exception e) {
@@ -170,6 +171,23 @@ public class JdbcConnection implements Connection {
         holdability = HOLD_CURSORS_OVER_COMMIT;
     }
 
+    private @Nullable SslConfiguration extractSslConfiguration(ConnectionProperties connProps) {
+        if (connProps.isSslEnabled()) {
+            return SslConfiguration.builder()
+                    .enabled(true)
+                    .trustStoreType(connProps.getTrustStoreType())
+                    .trustStorePath(connProps.getTrustStorePath())
+                    .trustStorePassword(connProps.getTrustStorePassword())
+                    .clientAuth(connProps.getClientAuth())
+                    .keyStoreType(connProps.getKeyStoreType())
+                    .keyStorePath(connProps.getKeyStorePath())
+                    .keyStorePassword(connProps.getKeyStorePassword())
+                    .build();
+        } else {
+            return null;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public Statement createStatement() throws SQLException {
diff --git a/modules/runner/build.gradle b/modules/runner/build.gradle
index a25bd533fd..1316da241d 100644
--- a/modules/runner/build.gradle
+++ b/modules/runner/build.gradle
@@ -114,6 +114,7 @@ dependencies {
     integrationTestImplementation project(':ignite-metastorage')
     integrationTestImplementation project(':ignite-table')
     integrationTestImplementation project(':ignite-transactions')
+    integrationTestImplementation project(':ignite-jdbc')
     integrationTestImplementation(testFixtures(project(':ignite-core')))
     integrationTestImplementation(testFixtures(project(':ignite-configuration')))
     integrationTestImplementation(testFixtures(project(':ignite-schema')))
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
index 9d4054eb73..4d19136f88 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
@@ -24,6 +24,9 @@ import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.client.IgniteClientConnectionException;
 import org.apache.ignite.client.SslConfiguration;
@@ -38,8 +41,6 @@ import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.api.TestInstance.Lifecycle;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 /** SSL support integration test. */
@@ -60,14 +61,9 @@ public class ItSslTest {
     }
 
     @Nested
-    @TestInstance(Lifecycle.PER_CLASS)
+    @DisplayName("Given SSL disabled on the cluster")
     class ClusterWithoutSsl {
 
-        @WorkDirectory
-        private Path workDir;
-
-        private Cluster cluster;
-
         @Language("JSON")
         String sslDisabledBoostrapConfig = "{\n"
                 + "  network: {\n"
@@ -80,6 +76,10 @@ public class ItSslTest {
                 + "  }\n"
                 + "}";
 
+        @WorkDirectory
+        private Path workDir;
+        private Cluster cluster;
+
         @BeforeEach
         void setUp(TestInfo testInfo) {
             cluster = new Cluster(testInfo, workDir, sslDisabledBoostrapConfig);
@@ -104,17 +104,53 @@ public class ItSslTest {
                 assertThat(client.clusterNodes(), hasSize(2));
             }
         }
+
+        @Test
+        @DisplayName("Client can not connect with ssl configured when ssl disabled on the cluster")
+        void clientCanNotConnectWithoutSsl() {
+            var sslConfiguration =
+                    SslConfiguration.builder()
+                            .enabled(true)
+                            .trustStoreType("JKS")
+                            .trustStorePath(trustStorePath)
+                            .trustStorePassword(password)
+                            .build();
+
+            assertThrows(
+                    IgniteClientConnectionException.class,
+                    () -> IgniteClient.builder()
+                            .addresses("localhost:10800")
+                            .ssl(sslConfiguration)
+                            .build()
+            );
+        }
+
+        @Test
+        @DisplayName("Jdbc driver could establish the connection when SSL disabled")
+        void jdbcCouldConnectWithoutSsl() throws SQLException {
+            var url = "jdbc:ignite:thin://127.0.0.1:10800";
+            try (Connection conn = DriverManager.getConnection(url)) {
+                // No-op.
+            }
+        }
+
+        @Test
+        @DisplayName("Jdbc client can not connect with SSL configured when SSL disabled on the server")
+        void jdbcCanNotConnectWithSsl() {
+            var url =
+                    "jdbc:ignite:thin://127.0.0.1:10800"
+                            + "?sslEnabled=true"
+                            + "&trustStorePath=" + trustStorePath
+                            + "&trustStoreType=JKS"
+                            + "&trustStorePassword=" + password;
+            assertThrows(SQLException.class, () -> DriverManager.getConnection(url));
+        }
     }
 
     @Nested
-    @TestInstance(Lifecycle.PER_CLASS)
+    @DisplayName("Given SSL enabled on the cluster")
     class ClusterWithSsl {
 
-        @WorkDirectory
-        private Path workDir;
-
-        private Cluster cluster;
-
         @Language("JSON")
         String sslEnabledBoostrapConfig = "{\n"
                 + "  network: {\n"
@@ -144,6 +180,10 @@ public class ItSslTest {
                 + "  }\n"
                 + "}";
 
+        @WorkDirectory
+        private Path workDir;
+        private Cluster cluster;
+
         @BeforeEach
         void setUp(TestInfo testInfo) {
             cluster = new Cluster(testInfo, workDir, sslEnabledBoostrapConfig);
@@ -171,6 +211,12 @@ public class ItSslTest {
             });
         }
 
+        @Test
+        void jdbcCannotConnectWithoutSsl() {
+            var url = "jdbc:ignite:thin://127.0.0.1:10800";
+            assertThrows(SQLException.class, () -> DriverManager.getConnection(url));
+        }
+
         @Test
         @DisplayName("Client can connect with SSL configured")
         void clientCanConnectWithSsl() throws Exception {
@@ -190,17 +236,26 @@ public class ItSslTest {
                 assertThat(client.clusterNodes(), hasSize(2));
             }
         }
+
+        @Test
+        @DisplayName("Jdbc client can connect with SSL configured")
+        void jdbcCanConnectWithSsl() throws SQLException {
+            var url =
+                    "jdbc:ignite:thin://127.0.0.1:10800"
+                            + "?sslEnabled=true"
+                            + "&trustStorePath=" + trustStorePath
+                            + "&trustStoreType=JKS"
+                            + "&trustStorePassword=" + password;
+            try (Connection conn = DriverManager.getConnection(url)) {
+                // No-op.
+            }
+        }
     }
 
     @Nested
-    @TestInstance(Lifecycle.PER_CLASS)
+    @DisplayName("Given SSL enabled client auth is set to require on the cluster")
     class ClusterWithSslAndClientAuth {
 
-        @WorkDirectory
-        private Path workDir;
-
-        private Cluster cluster;
-
         @Language("JSON")
         String sslEnabledBoostrapConfig = "{\n"
                 + "  network: {\n"
@@ -233,10 +288,14 @@ public class ItSslTest {
                 + "      type: JKS,"
                 + "      password: \"" + password + "\","
                 + "      path: \"" + trustStorePath + "\""
-                + "      },\n"
+                + "      }\n"
                 + "  }\n"
                 + "}";
 
+        @WorkDirectory
+        private Path workDir;
+        private Cluster cluster;
+
         @BeforeEach
         void setUp(TestInfo testInfo) {
             cluster = new Cluster(testInfo, workDir, sslEnabledBoostrapConfig);
@@ -264,6 +323,12 @@ public class ItSslTest {
             });
         }
 
+        @Test
+        void jdbcCannotConnectWithoutSsl() {
+            var url = "jdbc:ignite:thin://127.0.0.1:10800";
+            assertThrows(SQLException.class, () -> DriverManager.getConnection(url));
+        }
+
         @Test
         @DisplayName("Client can not connect without client authentication configured")
         void clientCanNotConnectWithoutClientAuth() {
@@ -305,5 +370,23 @@ public class ItSslTest {
                 assertThat(client.clusterNodes(), hasSize(2));
             }
         }
+
+        @Test
+        @DisplayName("Jdbc client can connect with SSL configured")
+        void jdbcCanConnectWithSslAndClientAuth() throws SQLException {
+            var url =
+                    "jdbc:ignite:thin://127.0.0.1:10800"
+                            + "?sslEnabled=true"
+                            + "&trustStorePath=" + trustStorePath
+                            + "&trustStoreType=JKS"
+                            + "&trustStorePassword=" + password
+                            + "&clientAuth=require"
+                            + "&keyStorePath=" + keyStorePath
+                            + "&keyStoreType=PKCS12"
+                            + "&keyStorePassword=" + password;
+            try (Connection conn = DriverManager.getConnection(url)) {
+                // No-op.
+            }
+        }
     }
 }