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 <[email protected]>
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);
+ }
}