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); + } }