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

apkhmv 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 a07835514f IGNITE-20676 Support cipher suites in CLI for REST and JDBC 
connections (#2812)
a07835514f is described below

commit a07835514fcdb6d67a187a08a237eb51751a799c
Author: Vadim Pakhnushev <8614891+valep...@users.noreply.github.com>
AuthorDate: Thu Nov 9 12:56:59 2023 +0300

    IGNITE-20676 Support cipher suites in CLI for REST and JDBC connections 
(#2812)
---
 .../org/apache/ignite/internal/NodeConfig.java     | 132 ++++++++++++---------
 .../cli/commands/connect/ItConnectCommandTest.java |  46 ++-----
 .../questions/ItConnectToSslClusterTest.java       |  12 +-
 .../cli/ssl/ItJdbcSslCustomCipherTest.java         |  99 ++++++++++++++++
 .../internal/cli/ssl/ItSslCustomCipherTest.java    |  96 +++++++++++++++
 .../internal/cli/call/connect/ConnectCall.java     |   5 +-
 .../cli/call/connect/ConnectionChecker.java        |  18 ++-
 .../ignite/internal/cli/config/CliConfigKeys.java  |  10 ++
 .../ignite/internal/cli/core/JdbcUrlFactory.java   |   2 +
 .../internal/cli/core/rest/ApiClientFactory.java   |  33 ++++--
 .../internal/cli/core/rest/ApiClientSettings.java  |  29 +++--
 .../cli/core/rest/ApiClientSettingsBuilder.java    |   8 +-
 .../internal/cli/core/JdbcUrlFactoryTest.java      |  18 +++
 13 files changed, 376 insertions(+), 132 deletions(-)

diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/NodeConfig.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/NodeConfig.java
index 798233bc45..20a8b93bc3 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/NodeConfig.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/NodeConfig.java
@@ -19,6 +19,9 @@ package org.apache.ignite.internal;
 
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.escapeWindowsPath;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.getResourcePath;
+import static org.apache.ignite.internal.util.StringUtils.nullOrBlank;
+
+import org.jetbrains.annotations.Nullable;
 
 /** Helper class which provides node configuration template for SSL. */
 public class NodeConfig {
@@ -31,58 +34,81 @@ public class NodeConfig {
     public static final String keyStorePassword = "changeit";
     public static final String trustStorePassword = "changeit";
 
-    /** Node bootstrap configuration pattern with SSL enabled. */
-    public static final String REST_SSL_BOOTSTRAP_CONFIG = "{\n"
-            + "  network: {\n"
-            + "    port: {},\n"
-            + "    nodeFinder: {\n"
-            + "      netClusterNodes: [ {} ]\n"
-            + "    },\n"
-            + "  },\n"
-            + "  clientConnector.port: {} ,\n"
-            + "  rest: {\n"
-            + "    port: {}\n"
-            + "    ssl: {\n"
-            + "      port: {},\n"
-            + "      enabled: true,\n"
-            + "      keyStore: {\n"
-            + "        path: \"" +  escapeWindowsPath(resolvedKeystorePath) + 
"\",\n"
-            + "        password: " + keyStorePassword + "\n"
-            + "      }, \n"
-            + "      trustStore: {\n"
-            + "        path: \"" + escapeWindowsPath(resolvedTruststorePath) + 
"\",\n"
-            + "        password: " + trustStorePassword + "\n"
-            + "      }\n"
-            + "    }\n"
-            + "  }\n"
-            + "}";
+    /** Node bootstrap configuration pattern with REST SSL enabled. */
+    public static final String REST_SSL_BOOTSTRAP_CONFIG = 
restSslBootstrapConfig(null);
+
+    /**
+     *  Node bootstrap configuration pattern with REST SSL enabled.
+     *
+     * @param ciphers Custom ciphers suites.
+     * @return Config pattern.
+     */
+    public static String restSslBootstrapConfig(@Nullable String ciphers) {
+        return "{\n"
+                + "  network: {\n"
+                + "    port: {},\n"
+                + "    nodeFinder: {\n"
+                + "      netClusterNodes: [ {} ]\n"
+                + "    },\n"
+                + "  },\n"
+                + "  clientConnector.port: {} ,\n"
+                + "  rest: {\n"
+                + "    port: {}\n"
+                + "    ssl: {\n"
+                + "      port: {},\n"
+                + "      enabled: true,\n"
+                + "      keyStore: {\n"
+                + "        path: \"" + escapeWindowsPath(resolvedKeystorePath) 
+ "\",\n"
+                + "        password: " + keyStorePassword + "\n"
+                + "      }, \n"
+                + "      trustStore: {\n"
+                + "        path: \"" + 
escapeWindowsPath(resolvedTruststorePath) + "\",\n"
+                + "        password: " + trustStorePassword + "\n"
+                + "      },\n"
+                + (nullOrBlank(ciphers) ? "" : "      ciphers: \"" + ciphers + 
"\"")
+                + "    }\n"
+                + "  }\n"
+                + "}";
+    }
+
+    /** Node bootstrap configuration pattern with client SSL enabled. */
+    public static final String CLIENT_CONNECTOR_SSL_BOOTSTRAP_CONFIG = 
clientConnectorSslBootstrapConfig(null);
 
-    public static final String CLIENT_CONNECTOR_SSL_BOOTSTRAP_CONFIG = "{\n"
-            + "  network: {\n"
-            + "    port: {},\n"
-            + "    nodeFinder: {\n"
-            + "      netClusterNodes: [ {} ]\n"
-            + "    },\n"
-            + "  },\n"
-            + "  clientConnector: {"
-            + "    port: {},\n"
-            + "    ssl: {\n"
-            + "      enabled: true,\n"
-            + "      clientAuth: require,\n"
-            + "      keyStore: {\n"
-            + "        path: \"" + escapeWindowsPath(resolvedKeystorePath) + 
"\",\n"
-            + "        password: " + keyStorePassword + "\n"
-            + "      }, \n"
-            + "      trustStore: {\n"
-            + "        type: JKS,\n"
-            + "        path: \"" + escapeWindowsPath(resolvedTruststorePath) + 
"\",\n"
-            + "        password: " + trustStorePassword + "\n"
-            + "      }\n"
-            + "    }\n"
-            + "  },\n"
-            + "  rest: {\n"
-            + "    port: {},\n"
-            + "    ssl.port: {}\n"
-            + "  }\n"
-            + "}";
+    /**
+     *  Node bootstrap configuration pattern with client SSL enabled.
+     *
+     * @param ciphers Custom ciphers suites.
+     * @return Config pattern.
+     */
+    public static String clientConnectorSslBootstrapConfig(@Nullable String 
ciphers) {
+        return "{\n"
+                + "  network: {\n"
+                + "    port: {},\n"
+                + "    nodeFinder: {\n"
+                + "      netClusterNodes: [ {} ]\n"
+                + "    },\n"
+                + "  },\n"
+                + "  clientConnector: {"
+                + "    port: {},\n"
+                + "    ssl: {\n"
+                + "      enabled: true,\n"
+                + "      clientAuth: require,\n"
+                + "      keyStore: {\n"
+                + "        path: \"" + escapeWindowsPath(resolvedKeystorePath) 
+ "\",\n"
+                + "        password: " + keyStorePassword + "\n"
+                + "      }, \n"
+                + "      trustStore: {\n"
+                + "        type: JKS,\n"
+                + "        path: \"" + 
escapeWindowsPath(resolvedTruststorePath) + "\",\n"
+                + "        password: " + trustStorePassword + "\n"
+                + "      },\n"
+                + (nullOrBlank(ciphers) ? "" : "      ciphers: \"" + ciphers + 
"\"")
+                + "    }\n"
+                + "  },\n"
+                + "  rest: {\n"
+                + "    port: {},\n"
+                + "    ssl.port: {}\n"
+                + "  }\n"
+                + "}";
+    }
 }
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectCommandTest.java
index ff332f9d54..df4120cb1b 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectCommandTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectCommandTest.java
@@ -20,30 +20,18 @@ package org.apache.ignite.internal.cli.commands.connect;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import jakarta.inject.Inject;
 import java.io.IOException;
-import 
org.apache.ignite.internal.cli.commands.CliCommandTestInitializedIntegrationBase;
-import org.apache.ignite.internal.cli.commands.TopLevelCliReplCommand;
-import org.apache.ignite.internal.cli.core.repl.prompt.PromptProvider;
+import org.apache.ignite.internal.cli.commands.ItConnectToClusterTestBase;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
-import picocli.CommandLine.Help.Ansi;
 
-class ItConnectCommandTest extends CliCommandTestInitializedIntegrationBase {
-    @Inject
-    PromptProvider promptProvider;
-
-    @Override
-    protected Class<?> getCommandClass() {
-        return TopLevelCliReplCommand.class;
-    }
+class ItConnectCommandTest extends ItConnectToClusterTestBase {
 
     @Test
     @DisplayName("Should connect to cluster with default url")
     void connectWithDefaultUrl() {
         // Given prompt before connect
-        String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptBefore).isEqualTo("[disconnected]> ");
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
 
         // When connect without parameters
         execute("connect");
@@ -54,8 +42,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
                 () -> assertOutputContains("Connected to 
http://localhost:10300";)
         );
         // And prompt is changed to connect
-        String promptAfter = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(getPrompt()).isEqualTo("[" + nodeName() + "]> ");
     }
 
     @Test
@@ -83,8 +70,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
                         + "Could not connect to node with URL 
http://localhost:11111"; + System.lineSeparator())
         );
         // And prompt is
-        String prompt = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(prompt).isEqualTo("[disconnected]> ");
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
     }
 
     @Test
@@ -93,8 +79,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
         // Given connected to cluster
         execute("connect");
         // And prompt is
-        String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptBefore).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(getPrompt()).isEqualTo("[" + nodeName() + "]> ");
 
         // When disconnect
         execute("disconnect");
@@ -104,8 +89,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
                 () -> assertOutputContains("Disconnected from 
http://localhost:10300";)
         );
         // And prompt is changed
-        String promptAfter = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptAfter).isEqualTo("[disconnected]> ");
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
     }
 
     @Test
@@ -119,8 +103,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
                 () -> assertOutputIs("Connected to http://localhost:10300"; + 
System.lineSeparator())
         );
         // And prompt is
-        String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptBefore).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(getPrompt()).isEqualTo("[" + nodeName() + "]> ");
 
         // When connect again
         resetOutput();
@@ -132,8 +115,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
                 () -> assertOutputIs("You are already connected to 
http://localhost:10300"; + System.lineSeparator())
         );
         // And prompt is still connected
-        String promptAfter = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(getPrompt()).isEqualTo("[" + nodeName() + "]> ");
     }
 
     @Test
@@ -141,8 +123,7 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
     void clusterWithoutAuthButUsernamePasswordProvided() throws IOException {
 
         // Given prompt before connect
-        String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(promptBefore).isEqualTo("[disconnected]> ");
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
 
         // When connect with auth parameters
         execute("connect", "--username", "admin", "--password", "password");
@@ -154,11 +135,6 @@ class ItConnectCommandTest extends 
CliCommandTestInitializedIntegrationBase {
         );
 
         // And prompt is
-        String prompt = Ansi.OFF.string(promptProvider.getPrompt());
-        assertThat(prompt).isEqualTo("[disconnected]> ");
-    }
-
-    private String nodeName() {
-        return CLUSTER_NODES.get(0).name();
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
     }
 }
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/questions/ItConnectToSslClusterTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/questions/ItConnectToSslClusterTest.java
index 8159abb42b..3ed391e80c 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/questions/ItConnectToSslClusterTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/questions/ItConnectToSslClusterTest.java
@@ -40,8 +40,7 @@ class ItConnectToSslClusterTest extends 
ItConnectToClusterTestBase {
     @DisplayName("Should connect to last connected cluster HTTPS url")
     void connectOnStart() throws IOException {
         // Given prompt before connect
-        String promptBefore = getPrompt();
-        assertThat(promptBefore).isEqualTo("[disconnected]> ");
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
 
         // And default URL is HTTPS
         
configManagerProvider.setConfigFile(TestConfigManagerHelper.createClusterUrlSslConfig());
@@ -64,16 +63,14 @@ class ItConnectToSslClusterTest extends 
ItConnectToClusterTestBase {
                 () -> assertOutputContains("Connected to 
https://localhost:10400";)
         );
         // And prompt is changed to connect
-        String promptAfter = getPrompt();
-        assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(getPrompt()).isEqualTo("[" + nodeName() + "]> ");
     }
 
     @Test
     @DisplayName("Should ask for SSL configuration connect to last connected 
cluster HTTPS url")
     void connectOnStartAskSsl() throws IOException {
         // Given prompt before connect
-        String promptBefore = getPrompt();
-        assertThat(promptBefore).isEqualTo("[disconnected]> ");
+        assertThat(getPrompt()).isEqualTo("[disconnected]> ");
 
         // And default URL is HTTPS
         
configManagerProvider.setConfigFile(TestConfigManagerHelper.createClusterUrlSslConfig());
@@ -96,8 +93,7 @@ class ItConnectToSslClusterTest extends 
ItConnectToClusterTestBase {
                 () -> assertOutputContains("Connected to 
https://localhost:10400";)
         );
         // And prompt is changed to connect
-        String promptAfter = getPrompt();
-        assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
+        assertThat(getPrompt()).isEqualTo("[" + nodeName() + "]> ");
 
         
assertThat(configManagerProvider.get().getCurrentProperty(CliConfigKeys.REST_TRUST_STORE_PATH.value()))
                 
.isEqualTo(escapeWindowsPath(NodeConfig.resolvedTruststorePath));
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItJdbcSslCustomCipherTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItJdbcSslCustomCipherTest.java
new file mode 100644
index 0000000000..600e8009e2
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItJdbcSslCustomCipherTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.ssl;
+
+import static 
org.apache.ignite.internal.NodeConfig.clientConnectorSslBootstrapConfig;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.apache.ignite.internal.NodeConfig;
+import 
org.apache.ignite.internal.cli.commands.CliCommandTestInitializedIntegrationBase;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for JDBC SSL. */
+public class ItJdbcSslCustomCipherTest extends 
CliCommandTestInitializedIntegrationBase {
+    private static final String CIPHER1 = "TLS_AES_256_GCM_SHA384";
+    private static final String CIPHER2 = 
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384";
+
+    @Override
+    protected String nodeBootstrapConfigTemplate() {
+        return clientConnectorSslBootstrapConfig(CIPHER1);
+    }
+
+    @BeforeEach
+    public void createTable() {
+        createAndPopulateTable();
+    }
+
+    @AfterEach
+    public void dropTables() {
+        dropAllTables();
+    }
+
+    @Test
+    void jdbcCompatibleCiphers() {
+        // Given valid JDBC connection string with SSL configured
+        String jdbcUrl = JDBC_URL
+                + "?sslEnabled=true"
+                + "&trustStorePath=" + NodeConfig.resolvedTruststorePath
+                + "&trustStoreType=JKS"
+                + "&trustStorePassword=" + NodeConfig.trustStorePassword
+                + "&clientAuth=require"
+                + "&keyStorePath=" + NodeConfig.resolvedKeystorePath
+                + "&keyStoreType=PKCS12"
+                + "&keyStorePassword=" + NodeConfig.keyStorePassword
+                + "&ciphers=" + CIPHER1;
+
+        // When
+        execute("sql", "--jdbc-url", jdbcUrl, "select * from person");
+
+        // Then the query is executed successfully
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertOutputIsNotEmpty,
+                this::assertErrOutputIsEmpty
+        );
+    }
+
+    @Test
+    void jdbcIncompatibleCiphers() {
+        // Given JDBC connection string with SSL configured but incompatible 
cipher
+        String jdbcUrl = JDBC_URL
+                + "?sslEnabled=true"
+                + "&trustStorePath=" + NodeConfig.resolvedTruststorePath
+                + "&trustStoreType=JKS"
+                + "&trustStorePassword=" + NodeConfig.trustStorePassword
+                + "&clientAuth=require"
+                + "&keyStorePath=" + NodeConfig.resolvedKeystorePath
+                + "&keyStoreType=PKCS12"
+                + "&keyStorePassword=" + NodeConfig.keyStorePassword
+                + "&ciphers=" + CIPHER2;
+
+        // When
+        execute("sql", "--jdbc-url", jdbcUrl, "select * from person");
+
+        // Then the query is executed successfully
+        assertAll(
+                () -> assertExitCodeIs(1),
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains("Connection failed"),
+                () -> assertErrOutputContains("Handshake error")
+        );
+    }
+}
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslCustomCipherTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslCustomCipherTest.java
new file mode 100644
index 0000000000..72bbb4e97e
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslCustomCipherTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.ssl;
+
+import static org.apache.ignite.internal.NodeConfig.restSslBootstrapConfig;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import jakarta.inject.Inject;
+import org.apache.ignite.internal.NodeConfig;
+import org.apache.ignite.internal.cli.call.connect.ConnectCall;
+import org.apache.ignite.internal.cli.call.connect.ConnectCallInput;
+import 
org.apache.ignite.internal.cli.commands.CliCommandTestNotInitializedIntegrationBase;
+import org.apache.ignite.internal.cli.config.CliConfigKeys;
+import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import org.junit.jupiter.api.Test;
+
+/** Tests for REST SSL. */
+public class ItSslCustomCipherTest extends 
CliCommandTestNotInitializedIntegrationBase {
+    private static final String CIPHER1 = "TLS_AES_256_GCM_SHA384";
+    private static final String CIPHER2 = 
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384";
+
+    @Inject
+    ConnectCall connectCall;
+
+    /** Mimics non-REPL "connect" command without starting REPL mode. 
Overriding getCommandClass and returning TopLevelCliReplCommand
+     * wouldn't help because it will start to ask questions.
+     */
+    private void connect(String url) {
+        Flows.from(ConnectCallInput.builder().url(url).build())
+                .then(Flows.fromCall(connectCall))
+                .print()
+                .start();
+    }
+
+    @Override
+    protected String nodeBootstrapConfigTemplate() {
+        return restSslBootstrapConfig(CIPHER1);
+    }
+
+    @Test
+    void compatibleCiphers() {
+        // When REST SSL is enabled
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_TRUST_STORE_PATH.value(),
 NodeConfig.resolvedTruststorePath);
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_TRUST_STORE_PASSWORD.value(),
 NodeConfig.trustStorePassword);
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_KEY_STORE_PATH.value(),
 NodeConfig.resolvedKeystorePath);
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_KEY_STORE_PASSWORD.value(),
 NodeConfig.keyStorePassword);
+
+        // And explicitly set the same cipher as for the node
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_CIPHERS.value(),
 CIPHER1);
+
+        // And connect via HTTPS
+        connect("https://localhost:10400";);
+
+        // Then
+        assertAll(
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputContains("Connected to 
https://localhost:10400";)
+        );
+    }
+
+    @Test
+    void incompatibleCiphers() {
+        // When REST SSL is enabled
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_TRUST_STORE_PATH.value(),
 NodeConfig.resolvedTruststorePath);
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_TRUST_STORE_PASSWORD.value(),
 NodeConfig.trustStorePassword);
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_KEY_STORE_PATH.value(),
 NodeConfig.resolvedKeystorePath);
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_KEY_STORE_PASSWORD.value(),
 NodeConfig.keyStorePassword);
+
+        // And explicitly set cipher different from the node
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.REST_CIPHERS.value(),
 CIPHER2);
+
+        // And connect via HTTPS
+        connect("https://localhost:10400";);
+
+        // Then
+        assertAll(
+                () -> assertErrOutputContains("SSL error"),
+                this::assertOutputIsEmpty
+        );
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCall.java
index 45338cce21..8b6cde15f0 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCall.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCall.java
@@ -94,7 +94,7 @@ public class ConnectCall implements Call<ConnectCallInput, 
String> {
         }
         try {
             // Try without authentication first to check whether the 
authentication is enabled on the cluster.
-            sessionInfo = connectWithoutAuthentication(nodeUrl);
+            sessionInfo = connectWithoutAuthentication(input);
             if (sessionInfo == null) {
                 // Try with authentication
                 sessionInfo = connectionChecker.checkConnection(input);
@@ -125,9 +125,8 @@ public class ConnectCall implements Call<ConnectCallInput, 
String> {
     }
 
     @Nullable
-    private SessionInfo connectWithoutAuthentication(String nodeUrl) throws 
ApiException {
+    private SessionInfo connectWithoutAuthentication(ConnectCallInput 
connectCallInput) throws ApiException {
         try {
-            ConnectCallInput connectCallInput = 
ConnectCallInput.builder().url(nodeUrl).build();
             return 
connectionChecker.checkConnectionWithoutAuthentication(connectCallInput);
         } catch (ApiException e) {
             if (e.getCause() == null && e.getCode() == 
HttpStatus.UNAUTHORIZED.getCode()) {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectionChecker.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectionChecker.java
index ae4ff40ce3..691301a73d 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectionChecker.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectionChecker.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.cli.call.connect;
 
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_PASSWORD;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_USERNAME;
+import static org.apache.ignite.internal.cli.config.CliConfigKeys.REST_CIPHERS;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.REST_KEY_STORE_PASSWORD;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.REST_KEY_STORE_PATH;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.REST_TRUST_STORE_PASSWORD;
@@ -125,15 +126,11 @@ public class ConnectionChecker {
         }
     }
 
-    private void buildSslSettings(SslConfig sslConfig, 
ApiClientSettingsBuilder settingsBuilder) {
-        if (sslConfig != null) {
-            settingsBuilder.keyStorePath(sslConfig.keyStorePath())
-                    .keyStorePassword(sslConfig.keyStorePassword())
-                    .trustStorePath(sslConfig.trustStorePath())
-                    .trustStorePassword(sslConfig.trustStorePassword());
-        } else {
-            buildSslSettingsFromConfig(settingsBuilder);
-        }
+    private static void buildSslSettings(SslConfig sslConfig, 
ApiClientSettingsBuilder settingsBuilder) {
+        settingsBuilder.keyStorePath(sslConfig.keyStorePath())
+                .keyStorePassword(sslConfig.keyStorePassword())
+                .trustStorePath(sslConfig.trustStorePath())
+                .trustStorePassword(sslConfig.trustStorePassword());
     }
 
     private void buildSslSettingsFromConfig(ApiClientSettingsBuilder 
settingsBuilder) {
@@ -141,7 +138,8 @@ public class ConnectionChecker {
         
settingsBuilder.keyStorePath(configManager.getCurrentProperty(REST_KEY_STORE_PATH.value()))
                 
.keyStorePassword(configManager.getCurrentProperty(REST_KEY_STORE_PASSWORD.value()))
                 
.trustStorePath(configManager.getCurrentProperty(REST_TRUST_STORE_PATH.value()))
-                
.trustStorePassword(configManager.getCurrentProperty(REST_TRUST_STORE_PASSWORD.value()));
+                
.trustStorePassword(configManager.getCurrentProperty(REST_TRUST_STORE_PASSWORD.value()))
+                
.ciphers(configManager.getCurrentProperty(REST_CIPHERS.value()));
     }
 
     /**
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
index 6f3d0ad599..f4945165c9 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/CliConfigKeys.java
@@ -42,6 +42,9 @@ public enum CliConfigKeys {
     /** REST key store password property name. */
     REST_KEY_STORE_PASSWORD(Constants.REST_KEY_STORE_PASSWORD),
 
+    /** REST SSL ciphers property name. */
+    REST_CIPHERS(Constants.REST_CIPHERS),
+
     /** Default JDBC URL property name. */
     JDBC_URL(Constants.JDBC_URL),
 
@@ -63,6 +66,9 @@ public enum CliConfigKeys {
     /** JDBC SSL client auth property name. */
     JDBC_CLIENT_AUTH(Constants.JDBC_CLIENT_AUTH),
 
+    /** JDBC SSL ciphers property name. */
+    JDBC_CIPHERS(Constants.JDBC_CIPHERS),
+
     /** Basic authentication username. */
     BASIC_AUTHENTICATION_USERNAME(Constants.BASIC_AUTHENTICATION_USERNAME),
 
@@ -112,6 +118,8 @@ public enum CliConfigKeys {
 
         public static final String REST_KEY_STORE_PASSWORD = 
"ignite.rest.key-store.password";
 
+        public static final String REST_CIPHERS = "ignite.rest.ciphers";
+
         public static final String JDBC_URL = "ignite.jdbc-url";
 
         public static final String JDBC_SSL_ENABLED = 
"ignite.jdbc.ssl-enabled";
@@ -126,6 +134,8 @@ public enum CliConfigKeys {
 
         public static final String JDBC_CLIENT_AUTH = 
"ignite.jdbc.client-auth";
 
+        public static final String JDBC_CIPHERS = "ignite.jdbc.ciphers";
+
         public static final String BASIC_AUTHENTICATION_USERNAME = 
"ignite.auth.basic.username";
 
         public static final String BASIC_AUTHENTICATION_PASSWORD = 
"ignite.auth.basic.password";
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/JdbcUrlFactory.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/JdbcUrlFactory.java
index 6cbe407520..c2672cf338 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/JdbcUrlFactory.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/JdbcUrlFactory.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.cli.core;
 
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_PASSWORD;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_USERNAME;
+import static org.apache.ignite.internal.cli.config.CliConfigKeys.JDBC_CIPHERS;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.JDBC_CLIENT_AUTH;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.JDBC_KEY_STORE_PASSWORD;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.JDBC_KEY_STORE_PATH;
@@ -72,6 +73,7 @@ public class JdbcUrlFactory {
         addIfSet(queryParams, JDBC_KEY_STORE_PATH, "keyStorePath");
         addIfSet(queryParams, JDBC_KEY_STORE_PASSWORD, "keyStorePassword");
         addIfSet(queryParams, JDBC_CLIENT_AUTH, "clientAuth");
+        addIfSet(queryParams, JDBC_CIPHERS, "ciphers");
         addSslEnabledIfNeeded(queryParams);
         addIfSet(queryParams, BASIC_AUTHENTICATION_USERNAME, 
"basicAuthenticationUsername");
         addIfSet(queryParams, BASIC_AUTHENTICATION_PASSWORD, 
"basicAuthenticationPassword");
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientFactory.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientFactory.java
index 5ac700ba2b..d10c5fea2e 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientFactory.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientFactory.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.cli.core.rest;
 
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_PASSWORD;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_USERNAME;
+import static org.apache.ignite.internal.cli.config.CliConfigKeys.REST_CIPHERS;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.REST_KEY_STORE_PASSWORD;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.REST_KEY_STORE_PATH;
 import static 
org.apache.ignite.internal.cli.config.CliConfigKeys.REST_TRUST_STORE_PASSWORD;
@@ -35,16 +36,20 @@ import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509TrustManager;
+import okhttp3.ConnectionSpec;
 import okhttp3.Interceptor;
 import okhttp3.OkHttpClient;
 import okhttp3.OkHttpClient.Builder;
@@ -98,20 +103,11 @@ public class ApiClientFactory {
                 
.keyStorePath(configManager.getCurrentProperty(REST_KEY_STORE_PATH.value()))
                 
.keyStorePassword(configManager.getCurrentProperty(REST_KEY_STORE_PASSWORD.value()))
                 
.trustStorePath(configManager.getCurrentProperty(REST_TRUST_STORE_PATH.value()))
-                
.trustStorePassword(configManager.getCurrentProperty(REST_TRUST_STORE_PASSWORD.value()));
+                
.trustStorePassword(configManager.getCurrentProperty(REST_TRUST_STORE_PASSWORD.value()))
+                
.ciphers(configManager.getCurrentProperty(REST_CIPHERS.value()));
         return setupAuthentication(builder).build();
     }
 
-    private ApiClientSettingsBuilder settingsBuilder(String path) {
-        ConfigManager configManager = configManagerProvider.get();
-        return ApiClientSettings.builder()
-                .basePath(path)
-                
.keyStorePath(configManager.getCurrentProperty(REST_KEY_STORE_PATH.value()))
-                
.keyStorePassword(configManager.getCurrentProperty(REST_KEY_STORE_PASSWORD.value()))
-                
.trustStorePath(configManager.getCurrentProperty(REST_TRUST_STORE_PATH.value()))
-                
.trustStorePassword(configManager.getCurrentProperty(REST_TRUST_STORE_PASSWORD.value()));
-    }
-
     private ApiClientSettingsBuilder 
setupAuthentication(ApiClientSettingsBuilder builder) {
         ConfigManager configManager = configManagerProvider.get();
 
@@ -191,6 +187,9 @@ public class ApiClientFactory {
 
         SSLContext sslContext = SSLContext.getInstance("TLS");
         sslContext.init(keyManagers, trustManagers, new SecureRandom());
+
+        setCiphers(builder, settings);
+
         return builder.sslSocketFactory(sslContext.getSocketFactory(), 
(X509TrustManager) trustManagers[0])
                 .hostnameVerifier(OkHostnameVerifier.INSTANCE);
     }
@@ -241,6 +240,18 @@ public class ApiClientFactory {
         }
     }
 
+    private static void setCiphers(Builder builder, ApiClientSettings 
settings) {
+        if (!nullOrBlank(settings.ciphers())) {
+            List<String> cipherSuites = 
Arrays.stream(settings.ciphers().split(","))
+                    .map(String::strip)
+                    .collect(Collectors.toList());
+            ConnectionSpec spec = new ConnectionSpec.Builder(true)
+                    .cipherSuites(cipherSuites.toArray(String[]::new))
+                    .build();
+            builder.connectionSpecs(List.of(spec));
+        }
+    }
+
     @Nullable
     private static Interceptor authInterceptor(ApiClientSettings settings) {
         if (!nullOrBlank(settings.basicAuthenticationUsername()) && 
!nullOrBlank(settings.basicAuthenticationPassword())) {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettings.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettings.java
index c631344636..a1fb9ff945 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettings.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettings.java
@@ -22,28 +22,30 @@ import java.util.Objects;
 /** Api client settings. */
 public class ApiClientSettings {
 
-    private String basePath;
+    private final String basePath;
 
-    private String keyStorePath;
+    private final String keyStorePath;
 
-    private String keyStorePassword;
+    private final String keyStorePassword;
 
-    private String trustStorePath;
+    private final String trustStorePath;
 
-    private String trustStorePassword;
+    private final String trustStorePassword;
 
-    private String basicAuthenticationUsername;
+    private final String ciphers;
 
-    private String basicAuthenticationPassword;
+    private final String basicAuthenticationUsername;
 
-    ApiClientSettings(String basePath, String keyStorePath, String 
keyStorePassword, String trustStorePath,
-            String trustStorePassword,
-            String basicAuthenticationUsername, String 
basicAuthenticationPassword) {
+    private final String basicAuthenticationPassword;
+
+    ApiClientSettings(String basePath, String keyStorePath, String 
keyStorePassword, String trustStorePath, String trustStorePassword,
+            String ciphers, String basicAuthenticationUsername, String 
basicAuthenticationPassword) {
         this.basePath = basePath;
         this.keyStorePath = keyStorePath;
         this.keyStorePassword = keyStorePassword;
         this.trustStorePath = trustStorePath;
         this.trustStorePassword = trustStorePassword;
+        this.ciphers = ciphers;
         this.basicAuthenticationUsername = basicAuthenticationUsername;
         this.basicAuthenticationPassword = basicAuthenticationPassword;
     }
@@ -72,6 +74,10 @@ public class ApiClientSettings {
         return trustStorePassword;
     }
 
+    public String ciphers() {
+        return ciphers;
+    }
+
     public String basicAuthenticationUsername() {
         return basicAuthenticationUsername;
     }
@@ -92,6 +98,7 @@ public class ApiClientSettings {
         return Objects.equals(basePath, that.basePath) && 
Objects.equals(keyStorePath, that.keyStorePath)
                 && Objects.equals(keyStorePassword, that.keyStorePassword) && 
Objects.equals(trustStorePath, that.trustStorePath)
                 && Objects.equals(trustStorePassword, that.trustStorePassword)
+                && Objects.equals(ciphers, that.ciphers)
                 && Objects.equals(basicAuthenticationUsername, 
that.basicAuthenticationUsername)
                 && Objects.equals(basicAuthenticationPassword, 
that.basicAuthenticationPassword);
     }
@@ -99,6 +106,6 @@ public class ApiClientSettings {
     @Override
     public int hashCode() {
         return Objects.hash(basePath, keyStorePath, keyStorePassword, 
trustStorePath, trustStorePassword,
-                basicAuthenticationUsername, basicAuthenticationPassword);
+                ciphers, basicAuthenticationUsername, 
basicAuthenticationPassword);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettingsBuilder.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettingsBuilder.java
index c31f0115c6..19ee5fb440 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettingsBuilder.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/rest/ApiClientSettingsBuilder.java
@@ -24,6 +24,7 @@ public class ApiClientSettingsBuilder {
     private String keyStorePassword;
     private String trustStorePath;
     private String trustStorePassword;
+    private String ciphers;
     private String basicAuthenticationUsername;
     private String basicAuthenticationPassword;
 
@@ -52,6 +53,11 @@ public class ApiClientSettingsBuilder {
         return this;
     }
 
+    public ApiClientSettingsBuilder ciphers(String ciphers) {
+        this.ciphers = ciphers;
+        return this;
+    }
+
     public ApiClientSettingsBuilder basicAuthenticationUsername(String 
basicAuthenticationUsername) {
         this.basicAuthenticationUsername = basicAuthenticationUsername;
         return this;
@@ -64,6 +70,6 @@ public class ApiClientSettingsBuilder {
 
     public ApiClientSettings build() {
         return new ApiClientSettings(basePath, keyStorePath, keyStorePassword, 
trustStorePath, trustStorePassword,
-                basicAuthenticationUsername, basicAuthenticationPassword);
+                ciphers, basicAuthenticationUsername, 
basicAuthenticationPassword);
     }
 }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/core/JdbcUrlFactoryTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/core/JdbcUrlFactoryTest.java
index b626afa008..3c4ee3d3c3 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/core/JdbcUrlFactoryTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/core/JdbcUrlFactoryTest.java
@@ -104,4 +104,22 @@ class JdbcUrlFactoryTest {
                 + "&basicAuthenticationPassword=pwd";
         assertEquals(expectedJdbcUrl, jdbcUrl);
     }
+
+    @Test
+    void withCustomCipher() {
+        // Given config with JDBC SSL and basic authentication enabled
+        configManagerProvider.setConfigFile(createIntegrationTestsConfig(), 
createJdbcTestsSslSecretConfig());
+        
configManagerProvider.configManager.setProperty(CliConfigKeys.JDBC_CIPHERS.value(),
 "TLS_AES_256_GCM_SHA384");
+
+        // Then JDBC URL is constructed with SSL settings and custom cipher
+        String jdbcUrl = factory.constructJdbcUrl("http://localhost:10300";, 
10800);
+        String expectedJdbcUrl = "jdbc:ignite:thin://localhost:10800"
+                + "?sslEnabled=true"
+                + "&trustStorePath=ssl/truststore.jks"
+                + "&trustStorePassword=changeit"
+                + "&keyStorePath=ssl/keystore.p12"
+                + "&keyStorePassword=changeit"
+                + "&ciphers=TLS_AES_256_GCM_SHA384";
+        assertEquals(expectedJdbcUrl, jdbcUrl);
+    }
 }


Reply via email to