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 cfe909d121 IGNITE-19936 Add username and password parameters to the
connect command (#2339)
cfe909d121 is described below
commit cfe909d1219da2d86ac8f661fc5002e66ea35681
Author: Dmitry Baranov <[email protected]>
AuthorDate: Sun Aug 13 21:03:04 2023 +0300
IGNITE-19936 Add username and password parameters to the connect command
(#2339)
---
.../cli/commands/connect/ItConnectCommandTest.java | 23 +++
...tConnectWithBasicAuthenticationCommandTest.java | 193 ++++++++++++++++++++-
.../apache/ignite/internal/cli/ssl/ItSslTest.java | 4 +-
.../internal/cli/call/connect/ConnectCall.java | 104 ++++++-----
.../cli/call/connect/ConnectCallInput.java | 105 +++++++++++
.../internal/cli/call/connect/ConnectSslCall.java | 7 +-
.../cli/call/connect/ConnectSslConfigCall.java | 3 +-
.../internal/cli/call/connect/DisconnectCall.java | 14 +-
.../ignite/internal/cli/commands/Options.java | 18 ++
.../cli/commands/connect/ConnectCommand.java | 16 +-
.../cli/commands/connect/ConnectOptions.java | 49 ++++++
.../cli/commands/connect/ConnectReplCommand.java | 28 ++-
.../questions/ConnectToClusterQuestion.java | 33 +++-
.../ignite/internal/cli/config/ConfigManager.java | 4 +
.../handler/IgniteCliApiExceptionHandler.java | 7 +-
.../cli/core/repl/ConnectionHeartBeat.java | 2 +-
.../ignite/internal/cli/core/repl/SessionInfo.java | 5 +-
.../internal/cli/core/rest/ApiClientFactory.java | 80 +++++++--
.../cli/commands/connect/ConnectCommandTest.java | 48 +++++
19 files changed, 662 insertions(+), 81 deletions(-)
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 123d45319b..ff332f9d54 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
@@ -21,6 +21,7 @@ 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;
@@ -135,6 +136,28 @@ class ItConnectCommandTest extends
CliCommandTestInitializedIntegrationBase {
assertThat(promptAfter).isEqualTo("[" + nodeName() + "]> ");
}
+ @Test
+ @DisplayName("Should throw error if cluster without authentication but
command invoked with username/password")
+ void clusterWithoutAuthButUsernamePasswordProvided() throws IOException {
+
+ // Given prompt before connect
+ String promptBefore = Ansi.OFF.string(promptProvider.getPrompt());
+ assertThat(promptBefore).isEqualTo("[disconnected]> ");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password", "password");
+
+ // Then
+ assertAll(
+ () -> assertErrOutputIs("Authentication is not enabled on
cluster but username or password were provided."
+ + System.lineSeparator())
+ );
+
+ // And prompt is
+ String prompt = Ansi.OFF.string(promptProvider.getPrompt());
+ assertThat(prompt).isEqualTo("[disconnected]> ");
+ }
+
private String nodeName() {
return CLUSTER_NODES.get(0).name();
}
diff --git
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectWithBasicAuthenticationCommandTest.java
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectWithBasicAuthenticationCommandTest.java
index 07d92aaaab..80fe7c2af3 100644
---
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectWithBasicAuthenticationCommandTest.java
+++
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/connect/ItConnectWithBasicAuthenticationCommandTest.java
@@ -22,13 +22,23 @@ import static
org.apache.ignite.internal.cli.commands.cliconfig.TestConfigManage
import static
org.apache.ignite.internal.cli.commands.cliconfig.TestConfigManagerHelper.readClusterConfigurationWithEnabledAuth;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import jakarta.inject.Inject;
+import java.io.IOException;
import org.apache.ignite.InitParametersBuilder;
import org.apache.ignite.internal.cli.commands.ItConnectToClusterTestBase;
import org.apache.ignite.internal.cli.config.CliConfigKeys;
+import org.apache.ignite.internal.cli.config.CliConfigKeys.Constants;
+import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
+import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class ItConnectWithBasicAuthenticationCommandTest extends
ItConnectToClusterTestBase {
+
+ @Inject
+ private ApiClientFactory apiClientFactory;
+
@Override
protected void configureInitParameters(InitParametersBuilder builder) {
builder.clusterConfiguration(readClusterConfigurationWithEnabledAuth());
@@ -48,7 +58,8 @@ class ItConnectWithBasicAuthenticationCommandTest extends
ItConnectToClusterTest
assertAll(
this::assertOutputIsEmpty,
() -> assertErrOutputIs("Authentication error" +
System.lineSeparator()
- + "Could not connect to node with URL
http://localhost:10300. Check authentication configuration"
+ + "Could not connect to node with URL
http://localhost:10300. "
+ + "Check authentication configuration or provided
username/password"
+ System.lineSeparator())
);
// And prompt is still disconnected
@@ -93,10 +104,188 @@ class ItConnectWithBasicAuthenticationCommandTest extends
ItConnectToClusterTest
assertAll(
this::assertOutputIsEmpty,
() -> assertErrOutputIs("Authentication error" +
System.lineSeparator()
- + "Could not connect to node with URL
http://localhost:10300. Check authentication configuration"
+ + "Could not connect to node with URL
http://localhost:10300. "
+ + "Check authentication configuration or provided
username/password"
+ System.lineSeparator())
);
// And prompt is still disconnected
assertThat(getPrompt()).isEqualTo("[disconnected]> ");
}
+
+ @Test
+ @DisplayName("Should connect to cluster with username/password")
+ void connectWithAuthenticationParameters() throws IOException {
+ // Given basic authentication is NOT configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig(),
createJdbcTestsBasicSecretConfig());
+
+ // Given prompt before connect
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password", "password");
+
+ // Then
+ assertAll(
+ this::assertErrOutputIsEmpty,
+ () -> assertOutputContains("Connected to
http://localhost:10300")
+ );
+
+ // And prompt shows user name and node name
+ assertThat(getPrompt()).isEqualTo("[admin:" + nodeName() + "]> ");
+ }
+
+ @Test
+ @DisplayName("Should NOT connect to cluster with incorrect password")
+ void connectWithWrongAuthenticationParameters() {
+ // Given basic authentication is NOT configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig());
+
+ // Given prompt before connect
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password",
"wrong-password");
+
+ // Then
+ assertAll(
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputIs("Authentication error" +
System.lineSeparator()
+ + "Could not connect to node with URL
http://localhost:10300. "
+ + "Check authentication configuration or provided
username/password"
+ + System.lineSeparator())
+ );
+ // And prompt is still disconnected
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+ }
+
+ @Test
+ void connectFailIfPasswordNotDefined() {
+ // Given basic authentication is NOT configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig());
+
+ // Given prompt before connect
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password", "");
+
+ // Then
+ assertAll(
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputIs("Authentication error" +
System.lineSeparator()
+ + "Could not connect to node with URL
http://localhost:10300. "
+ + "Check authentication configuration or provided
username/password"
+ + System.lineSeparator())
+ );
+ // And prompt is still disconnected
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+ }
+
+ @Test
+ @DisplayName("Should connect to cluster with incorrect password in config
but correct in command")
+ void connectWithWrongAuthenticationParametersInConfig() throws IOException
{
+ // Given basic authentication is configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig(),
createJdbcTestsBasicSecretConfig());
+ // And wrong password is in config
+
configManagerProvider.configManager.setProperty(CliConfigKeys.Constants.BASIC_AUTHENTICATION_PASSWORD,
"wrong-password");
+
+ // Given prompt before connect
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+
+ // And answer is "y"
+ bindAnswers("y");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password", "password");
+
+ // Then
+ assertAll(
+ this::assertErrOutputIsEmpty,
+ () -> assertOutputIs(
+ "Config saved" + System.lineSeparator() + "Connected
to http://localhost:10300" + System.lineSeparator())
+ );
+
+ // And prompt shows user name and node name
+ assertThat(getPrompt()).isEqualTo("[admin:" + nodeName() + "]> ");
+ }
+
+ @Test
+ @DisplayName("Should restore initial values in config in case of connect
failed")
+ void connectWithWrongAuthenticationParametersRestorePreviousCredentials() {
+ // Given basic authentication is configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig(),
createJdbcTestsBasicSecretConfig());
+
+ // Given prompt before connect
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password",
"wrong-password");
+
+ // Then
+ assertAll(
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputIs("Authentication error" +
System.lineSeparator()
+ + "Could not connect to node with URL
http://localhost:10300. "
+ + "Check authentication configuration or provided
username/password"
+ + System.lineSeparator())
+ );
+ // And prompt is still disconnected
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+ //Previous correct values restored in config
+ assertEquals("admin",
configManagerProvider.get().getCurrentProperty(Constants.BASIC_AUTHENTICATION_USERNAME));
+ assertEquals("password",
configManagerProvider.get().getCurrentProperty(Constants.BASIC_AUTHENTICATION_PASSWORD));
+ }
+
+ @Test
+ @DisplayName("Should ask to store credentials")
+ void shouldAskToStoreCredentials() throws IOException {
+ // Given basic authentication is NOT configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig());
+ // Given prompt before connect
+ String promptBefore = getPrompt();
+ assertThat(promptBefore).isEqualTo("[disconnected]> ");
+
+ // And answer is "y"
+ bindAnswers("y");
+
+ // And connected
+ execute("connect", "--username", "admin", "--password", "password");
+
+ // And output is
+ assertAll(
+ this::assertErrOutputIsEmpty,
+ () -> assertOutputIs(
+ "Config saved" + System.lineSeparator() + "Connected
to http://localhost:10300" + System.lineSeparator())
+ );
+
+ // And prompt shows user name and node name
+ assertThat(getPrompt()).isEqualTo("[admin:" + nodeName() + "]> ");
+ }
+
+ @Test
+ @DisplayName("Should create correct api client even if user doesn't store
credentials in settings.")
+ void sessionListenersShouldBeInvokedWithCorrectCredentials() throws
IOException {
+ // Given basic authentication is configured in config file
+ configManagerProvider.setConfigFile(createIntegrationTestsConfig(),
createJdbcTestsBasicSecretConfig());
+ // And wrong password is in config
+
configManagerProvider.configManager.setProperty(CliConfigKeys.Constants.BASIC_AUTHENTICATION_PASSWORD,
"wrong-password");
+
+ // Given prompt before connect
+ assertThat(getPrompt()).isEqualTo("[disconnected]> ");
+
+ // And answer is "n"
+ bindAnswers("n");
+
+ // When connect with auth parameters
+ execute("connect", "--username", "admin", "--password", "password");
+
+ // Then
+ assertAll(
+ this::assertErrOutputIsEmpty,
+ () -> assertThat(getPrompt()).isEqualTo("[admin:" + nodeName()
+ "]> "),
+ () -> assertEquals("password",
apiClientFactory.currentSessionSettings().basicAuthenticationPassword()),
+ () -> assertEquals("wrong-password",
+
configManagerProvider.get().getCurrentProperty(Constants.BASIC_AUTHENTICATION_PASSWORD))
+ );
+ }
}
diff --git
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslTest.java
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslTest.java
index 2a5c5bc70e..4d3a7ae52f 100644
---
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslTest.java
+++
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/ssl/ItSslTest.java
@@ -22,7 +22,7 @@ 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.core.call.UrlCallInput;
+import org.apache.ignite.internal.cli.call.connect.ConnectCallInput;
import org.apache.ignite.internal.cli.core.flow.builder.Flows;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -36,7 +36,7 @@ public class ItSslTest extends
CliSslNotInitializedIntegrationTestBase {
* wouldn't help because it will start to ask questions.
*/
private void connect(String url) {
- Flows.from(new UrlCallInput(url))
+ Flows.from(ConnectCallInput.builder().url(url).build())
.then(Flows.fromCall(connectCall))
.print()
.start();
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 91eb428084..91c6def854 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
@@ -17,31 +17,33 @@
package org.apache.ignite.internal.cli.call.connect;
+import static
org.apache.ignite.internal.cli.config.CliConfigKeys.BASIC_AUTHENTICATION_USERNAME;
import static org.apache.ignite.lang.util.StringUtils.nullOrBlank;
import io.micronaut.http.HttpStatus;
import jakarta.inject.Singleton;
import java.util.Objects;
import org.apache.ignite.internal.cli.config.CliConfigKeys;
+import org.apache.ignite.internal.cli.config.ConfigManagerProvider;
import org.apache.ignite.internal.cli.config.StateConfigProvider;
import org.apache.ignite.internal.cli.core.JdbcUrlFactory;
import org.apache.ignite.internal.cli.core.call.Call;
import org.apache.ignite.internal.cli.core.call.CallOutput;
import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
-import
org.apache.ignite.internal.cli.core.exception.handler.IgniteCliApiExceptionHandler;
+import org.apache.ignite.internal.cli.core.exception.IgniteCliException;
import org.apache.ignite.internal.cli.core.repl.Session;
import org.apache.ignite.internal.cli.core.repl.SessionInfo;
import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
+import org.apache.ignite.internal.cli.core.rest.ApiClientSettings;
import org.apache.ignite.internal.cli.core.style.component.MessageUiComponent;
import org.apache.ignite.internal.cli.core.style.element.UiElements;
import org.apache.ignite.internal.cli.event.EventPublisher;
import org.apache.ignite.internal.cli.event.Events;
import org.apache.ignite.rest.client.api.NodeConfigurationApi;
import org.apache.ignite.rest.client.api.NodeManagementApi;
+import org.apache.ignite.rest.client.invoker.ApiClient;
import org.apache.ignite.rest.client.invoker.ApiException;
-import org.apache.ignite.rest.client.model.Problem;
import org.jetbrains.annotations.Nullable;
@@ -49,7 +51,7 @@ import org.jetbrains.annotations.Nullable;
* Call for connect to Ignite 3 node. As a result {@link Session} will hold a
valid node-url.
*/
@Singleton
-public class ConnectCall implements Call<UrlCallInput, String> {
+public class ConnectCall implements Call<ConnectCallInput, String> {
private final Session session;
private final StateConfigProvider stateConfigProvider;
@@ -58,40 +60,59 @@ public class ConnectCall implements Call<UrlCallInput,
String> {
private final JdbcUrlFactory jdbcUrlFactory;
+ private final ConfigManagerProvider configManagerProvider;
+
private final EventPublisher eventPublisher;
/**
* Constructor.
*/
public ConnectCall(Session session, StateConfigProvider
stateConfigProvider, ApiClientFactory clientFactory,
- JdbcUrlFactory jdbcUrlFactory, EventPublisher eventPublisher) {
+ JdbcUrlFactory jdbcUrlFactory, ConfigManagerProvider
configManagerProvider,
+ EventPublisher eventPublisher) {
this.session = session;
this.stateConfigProvider = stateConfigProvider;
this.clientFactory = clientFactory;
this.jdbcUrlFactory = jdbcUrlFactory;
+ this.configManagerProvider = configManagerProvider;
this.eventPublisher = eventPublisher;
}
@Override
- public CallOutput<String> execute(UrlCallInput input) {
- String nodeUrl = input.getUrl();
+ public CallOutput<String> execute(ConnectCallInput input) {
+ String nodeUrl = input.url();
SessionInfo sessionInfo = session.info();
if (sessionInfo != null && Objects.equals(sessionInfo.nodeUrl(),
nodeUrl)) {
MessageUiComponent message = MessageUiComponent.fromMessage("You
are already connected to %s", UiElements.url(nodeUrl));
return DefaultCallOutput.success(message.render());
}
try {
- String username = getAuthenticatedUsername(nodeUrl);
- String configuration = fetchNodeConfiguration(nodeUrl);
+ // Try without authentication first to check whether the
authentication is enabled on the cluster.
+ sessionInfo = connectWithoutAuthentication(nodeUrl);
+ if (sessionInfo == null) {
+ // Try with authentication
+ if (!nullOrBlank(input.username()) &&
!nullOrBlank(input.password())) {
+ sessionInfo = connectWithAuthentication(nodeUrl,
input.username(), input.password());
+
+ // Use current credentials as default for api clients
+ ApiClientSettings clientSettings =
ApiClientSettings.builder()
+ .basicAuthenticationUsername(input.username())
+ .basicAuthenticationPassword(input.password())
+ .build();
+ clientFactory.setSessionSettings(clientSettings);
+ } else {
+ sessionInfo = connectWithAuthentication(nodeUrl);
+ }
+ } else if (!nullOrBlank(input.username()) ||
!nullOrBlank(input.password())) {
+ // Cluster without authentication but connect command invoked
with username/password
+ return DefaultCallOutput.failure(
+ handleException(new IgniteCliException(
+ "Authentication is not enabled on cluster but
username or password were provided."),
+ sessionInfo.nodeUrl()));
+ }
+
stateConfigProvider.get().setProperty(CliConfigKeys.LAST_CONNECTED_URL.value(),
nodeUrl);
- String jdbcUrl = jdbcUrlFactory.constructJdbcUrl(configuration,
nodeUrl);
- sessionInfo = SessionInfo.builder()
- .nodeUrl(nodeUrl)
- .nodeName(fetchNodeName(nodeUrl))
- .jdbcUrl(jdbcUrl)
- .username(username)
- .build();
eventPublisher.publish(Events.connect(sessionInfo));
return
DefaultCallOutput.success(MessageUiComponent.fromMessage("Connected to %s",
UiElements.url(nodeUrl)).render());
@@ -103,41 +124,36 @@ public class ConnectCall implements Call<UrlCallInput,
String> {
}
}
- private String fetchNodeName(String nodeUrl) throws ApiException {
- return new
NodeManagementApi(clientFactory.getClient(nodeUrl)).nodeState().getName();
- }
-
- /**
- * Deduces whether the cluster has the authentication turned on and
returns a name of successfully authenticated user or {@code null}.
- *
- * @param nodeUrl Node URL.
- * @return Username if authentication was successful or {@code null} if
not or if the cluster has no authentication turned on.
- * @throws ApiException If fails to call an API.
- */
@Nullable
- private String getAuthenticatedUsername(String nodeUrl) throws
ApiException {
- // Try without authentication first to check whether the
authentication is enabled on the cluster.
- String username = clientFactory.basicAuthenticationUsername();
- if (!nullOrBlank(username)) {
- try {
- new
NodeConfigurationApi(clientFactory.getClientWithoutBasicAuthentication(nodeUrl)).getNodeConfiguration();
+ private SessionInfo connectWithoutAuthentication(String nodeUrl) throws
ApiException {
+ try {
+ ApiClient apiClient =
clientFactory.getClientWithoutBasicAuthentication(nodeUrl);
+ return constructSessionInfo(apiClient, nodeUrl, null);
+ } catch (ApiException e) {
+ if (e.getCause() == null && e.getCode() ==
HttpStatus.UNAUTHORIZED.getCode()) {
return null;
- } catch (ApiException e) {
- if (e.getCause() == null) {
- Problem problem =
IgniteCliApiExceptionHandler.extractProblem(e);
- if (problem.getStatus() ==
HttpStatus.UNAUTHORIZED.getCode()) {
- new
NodeConfigurationApi(clientFactory.getClient(nodeUrl)).getNodeConfiguration();
- return username;
- }
- }
+ } else {
throw e;
}
}
- return null;
}
- private String fetchNodeConfiguration(String nodeUrl) throws ApiException {
- return new
NodeConfigurationApi(clientFactory.getClient(nodeUrl)).getNodeConfiguration();
+ private SessionInfo connectWithAuthentication(String nodeUrl, String
inputUsername, String inputPassword) throws ApiException {
+ ApiClient apiClient = clientFactory.getClient(nodeUrl, inputUsername,
inputPassword);
+ return constructSessionInfo(apiClient, nodeUrl, inputUsername);
+ }
+
+ private SessionInfo connectWithAuthentication(String nodeUrl) throws
ApiException {
+ ApiClient apiClient = clientFactory.getClient(nodeUrl);
+ String username =
configManagerProvider.get().getCurrentProperty(BASIC_AUTHENTICATION_USERNAME.value());
+ return constructSessionInfo(apiClient, nodeUrl, username);
+ }
+
+ private SessionInfo constructSessionInfo(ApiClient apiClient, String
nodeUrl, @Nullable String username) throws ApiException {
+ String configuration = new
NodeConfigurationApi(apiClient).getNodeConfiguration();
+ String nodeName = new
NodeManagementApi(apiClient).nodeState().getName();
+ String jdbcUrl = jdbcUrlFactory.constructJdbcUrl(configuration,
nodeUrl);
+ return
SessionInfo.builder().nodeUrl(nodeUrl).nodeName(nodeName).jdbcUrl(jdbcUrl).username(username).build();
}
private static IgniteCliApiException handleException(Exception e, String
nodeUrl) {
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCallInput.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCallInput.java
new file mode 100644
index 0000000000..207eea1e7d
--- /dev/null
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectCallInput.java
@@ -0,0 +1,105 @@
+/*
+ * 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.call.connect;
+
+import org.apache.ignite.internal.cli.core.call.CallInput;
+import org.jetbrains.annotations.Nullable;
+
+/** Input for the {@link ConnectCall} call. */
+public class ConnectCallInput implements CallInput {
+
+ private final String url;
+
+ @Nullable
+ private final String username;
+
+ @Nullable
+ private final String password;
+
+ private ConnectCallInput(String url, @Nullable String username, @Nullable
String password) {
+ this.url = url;
+ this.username = username;
+ this.password = password;
+ }
+
+ String url() {
+ return url;
+ }
+
+ /**
+ * Provided username.
+ *
+ * @return username
+ */
+ @Nullable
+ String username() {
+ return username;
+ }
+
+ /**
+ * Provided password.
+ *
+ * @return password
+ */
+ @Nullable
+ String password() {
+ return password;
+ }
+
+ /**
+ * Builder method provider.
+ *
+ * @return new instance of {@link ConnectCallInputBuilder}.
+ */
+ public static ConnectCallInputBuilder builder() {
+ return new ConnectCallInputBuilder();
+ }
+
+ /** Builder for {@link ConnectCallInput}. */
+ public static class ConnectCallInputBuilder {
+
+ private String url;
+ @Nullable
+ private String username;
+
+ @Nullable
+ private String password;
+
+ private ConnectCallInputBuilder() {
+ }
+
+ public ConnectCallInputBuilder url(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public ConnectCallInputBuilder username(@Nullable String username) {
+ this.username = username;
+ return this;
+ }
+
+ public ConnectCallInputBuilder password(@Nullable String password) {
+ this.password = password;
+ return this;
+ }
+
+ public ConnectCallInput build() {
+ return new ConnectCallInput(url, username, password);
+ }
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslCall.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslCall.java
index 214227b074..5047bcac89 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslCall.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslCall.java
@@ -23,7 +23,6 @@ import javax.net.ssl.SSLException;
import
org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
import org.apache.ignite.internal.cli.core.call.Call;
import org.apache.ignite.internal.cli.core.call.CallOutput;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
import org.apache.ignite.internal.cli.core.flow.Flowable;
import org.apache.ignite.internal.cli.core.flow.builder.FlowBuilder;
import org.apache.ignite.rest.client.invoker.ApiException;
@@ -32,7 +31,7 @@ import org.apache.ignite.rest.client.invoker.ApiException;
* Call which tries to connect to the Ignite 3 node and in case of SSL error
asks the user for the SSL configuration and tries again.
*/
@Singleton
-public class ConnectSslCall implements Call<UrlCallInput, String> {
+public class ConnectSslCall implements Call<ConnectCallInput, String> {
@Inject
private ConnectCall connectCall;
@@ -40,7 +39,7 @@ public class ConnectSslCall implements Call<UrlCallInput,
String> {
private ConnectSslConfigCall connectSslConfigCall;
@Override
- public CallOutput<String> execute(UrlCallInput input) {
+ public CallOutput<String> execute(ConnectCallInput input) {
CallOutput<String> output = connectCall.execute(input);
if (output.hasError()) {
if (output.errorCause().getCause() instanceof ApiException) {
@@ -50,7 +49,7 @@ public class ConnectSslCall implements Call<UrlCallInput,
String> {
FlowBuilder<Void, SslConfig> flowBuilder =
ConnectToClusterQuestion.askQuestionOnSslError();
Flowable<SslConfig> result =
flowBuilder.build().start(Flowable.empty());
if (result.hasResult()) {
- return connectSslConfigCall.execute(new
ConnectSslConfigCallInput(input.getUrl(), result.value()));
+ return connectSslConfigCall.execute(new
ConnectSslConfigCallInput(input.url(), result.value()));
}
}
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslConfigCall.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslConfigCall.java
index a3eedfabe1..d7c1b5b299 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslConfigCall.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/ConnectSslConfigCall.java
@@ -25,7 +25,6 @@ import
org.apache.ignite.internal.cli.config.ConfigManagerProvider;
import org.apache.ignite.internal.cli.core.call.Call;
import org.apache.ignite.internal.cli.core.call.CallOutput;
import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
import org.apache.ignite.internal.cli.core.rest.ApiClientSettings;
@@ -50,7 +49,7 @@ public class ConnectSslConfigCall implements
Call<ConnectSslConfigCallInput, Str
try {
checkConnection(input);
saveConfig(input.getConfig());
- return connectCall.execute(new UrlCallInput(input.getUrl()));
+ return
connectCall.execute(ConnectCallInput.builder().url(input.getUrl()).build());
} catch (ApiException e) {
return DefaultCallOutput.failure(new IgniteCliApiException(e,
input.getUrl()));
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/DisconnectCall.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/DisconnectCall.java
index e16dc3e590..e02a8cb9a7 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/DisconnectCall.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/connect/DisconnectCall.java
@@ -24,6 +24,7 @@ import
org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
import org.apache.ignite.internal.cli.core.call.EmptyCallInput;
import org.apache.ignite.internal.cli.core.repl.Session;
import org.apache.ignite.internal.cli.core.repl.SessionInfo;
+import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
import org.apache.ignite.internal.cli.core.style.component.MessageUiComponent;
import org.apache.ignite.internal.cli.core.style.element.UiElements;
import org.apache.ignite.internal.cli.event.EventPublisher;
@@ -38,9 +39,19 @@ public class DisconnectCall implements Call<EmptyCallInput,
String> {
private final EventPublisher eventPublisher;
- public DisconnectCall(Session session, EventPublisher eventPublisher) {
+ private final ApiClientFactory clientFactory;
+
+ /**
+ * Creates Disconnect call.
+ *
+ * @param session session
+ * @param eventPublisher event publisher
+ * @param clientFactory client factory
+ */
+ public DisconnectCall(Session session, EventPublisher eventPublisher,
ApiClientFactory clientFactory) {
this.session = session;
this.eventPublisher = eventPublisher;
+ this.clientFactory = clientFactory;
}
@Override
@@ -48,6 +59,7 @@ public class DisconnectCall implements Call<EmptyCallInput,
String> {
SessionInfo sessionInfo = session.info();
if (sessionInfo != null) {
String nodeUrl = sessionInfo.nodeUrl();
+ clientFactory.setSessionSettings(null);
eventPublisher.publish(Events.disconnect());
return DefaultCallOutput.success(
MessageUiComponent.fromMessage("Disconnected from %s",
UiElements.url(nodeUrl)).render()
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
index e2c8bb3d40..57a6ff0532 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/Options.java
@@ -266,5 +266,23 @@ public enum Options {
public static final String CLUSTER_CONFIG_FILE_OPTION_SHORT = "-cfgf";
public static final String CLUSTER_CONFIG_FILE_OPTION_DESC = "Path to
cluster configuration file";
+
+ public static final String PASSWORD_OPTION = "--password";
+
+ public static final String PASSWORD_OPTION_SHORT = "-p";
+
+ public static final String PASSWORD_OPTION_DESC = "Password to connect
to cluster";
+
+ public static final String USERNAME_OPTION = "--username";
+
+ public static final String USERNAME_OPTION_SHORT = "-u";
+
+ public static final String USERNAME_OPTION_DESC = "Username to connect
to cluster";
+
+ public static final String USERNAME_KEY =
CliConfigKeys.Constants.BASIC_AUTHENTICATION_USERNAME;
+
+ public static final String PASSWORD_KEY =
CliConfigKeys.Constants.BASIC_AUTHENTICATION_PASSWORD;
+
+
}
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommand.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommand.java
index 234040630c..eb3e526582 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommand.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommand.java
@@ -24,10 +24,11 @@ import java.net.URL;
import java.util.concurrent.Callable;
import org.apache.ignite.internal.cli.ReplManager;
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.BaseCommand;
import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
import org.apache.ignite.internal.cli.core.converters.UrlConverter;
+import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
@@ -41,6 +42,9 @@ public class ConnectCommand extends BaseCommand implements
Callable<Integer> {
@Parameters(description = NODE_URL_OPTION_DESC, converter =
UrlConverter.class)
private URL nodeUrl;
+ @ArgGroup(exclusive = false)
+ private ConnectOptions connectOptions;
+
@Inject
private ConnectCall connectCall;
@@ -51,7 +55,7 @@ public class ConnectCommand extends BaseCommand implements
Callable<Integer> {
@Override
public Integer call() {
int exitCode = CallExecutionPipeline.builder(connectCall)
- .inputProvider(() -> new UrlCallInput(nodeUrl.toString()))
+ .inputProvider(this::connectCallInput)
.output(spec.commandLine().getOut())
.errOutput(spec.commandLine().getErr())
.verbose(verbose)
@@ -62,4 +66,12 @@ public class ConnectCommand extends BaseCommand implements
Callable<Integer> {
}
return exitCode;
}
+
+ private ConnectCallInput connectCallInput() {
+ return ConnectCallInput.builder()
+ .url(nodeUrl.toString())
+ .username(connectOptions != null ? connectOptions.username() :
null)
+ .password(connectOptions != null ? connectOptions.password() :
null)
+ .build();
+ }
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectOptions.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectOptions.java
new file mode 100644
index 0000000000..b6c6827918
--- /dev/null
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectOptions.java
@@ -0,0 +1,49 @@
+/*
+ * 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.commands.connect;
+
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.PASSWORD_OPTION;
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.PASSWORD_OPTION_DESC;
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.PASSWORD_OPTION_SHORT;
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.USERNAME_OPTION;
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.USERNAME_OPTION_DESC;
+import static
org.apache.ignite.internal.cli.commands.Options.Constants.USERNAME_OPTION_SHORT;
+
+import picocli.CommandLine.Option;
+
+/**
+ * Mixin class for connect command options.
+ */
+public class ConnectOptions {
+
+ @Option(names = {USERNAME_OPTION,
+ USERNAME_OPTION_SHORT}, description = USERNAME_OPTION_DESC,
required = true)
+ private String username;
+
+ @Option(names = {PASSWORD_OPTION,
+ PASSWORD_OPTION_SHORT}, description = PASSWORD_OPTION_DESC,
required = true)
+ private String password;
+
+ public String username() {
+ return username;
+ }
+
+ public String password() {
+ return password;
+ }
+}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectReplCommand.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectReplCommand.java
index 9afd89d224..ca5d511b12 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectReplCommand.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/connect/ConnectReplCommand.java
@@ -21,12 +21,14 @@ import static
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_
import static
org.apache.ignite.internal.cli.commands.Options.Constants.NODE_URL_OR_NAME_DESC;
import jakarta.inject.Inject;
+import org.apache.ignite.internal.cli.call.connect.ConnectCallInput;
import org.apache.ignite.internal.cli.call.connect.ConnectSslCall;
import org.apache.ignite.internal.cli.commands.BaseCommand;
import org.apache.ignite.internal.cli.commands.node.NodeNameOrUrl;
import
org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import org.jetbrains.annotations.Nullable;
+import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
@@ -40,6 +42,9 @@ public class ConnectReplCommand extends BaseCommand
implements Runnable {
@Parameters(description = NODE_URL_OR_NAME_DESC, descriptionKey =
CLUSTER_URL_KEY)
private NodeNameOrUrl nodeNameOrUrl;
+ @ArgGroup(exclusive = false)
+ private ConnectOptions connectOptions;
+
@Inject
private ConnectSslCall connectCall;
@@ -50,10 +55,29 @@ public class ConnectReplCommand extends BaseCommand
implements Runnable {
@Override
public void run() {
question.askQuestionIfConnected(nodeNameOrUrl.stringUrl())
- .map(UrlCallInput::new)
+ .map(this::connectCallInput)
.then(Flows.fromCall(connectCall))
+ .onSuccess(() ->
question.askQuestionToStoreCredentials(username(connectOptions),
password(connectOptions)))
.verbose(verbose)
.print()
.start();
}
+
+ @Nullable
+ private String username(ConnectOptions connectOptions) {
+ return connectOptions != null ? connectOptions.username() : null;
+ }
+
+ @Nullable
+ private String password(ConnectOptions connectOptions) {
+ return connectOptions != null ? connectOptions.password() : null;
+ }
+
+ private ConnectCallInput connectCallInput(String nodeUrl) {
+ return ConnectCallInput.builder()
+ .url(nodeUrl)
+ .username(username(connectOptions))
+ .password(password(connectOptions))
+ .build();
+ }
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/questions/ConnectToClusterQuestion.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/questions/ConnectToClusterQuestion.java
index 8f2c02ce79..3b2905d6a8 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/questions/ConnectToClusterQuestion.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/questions/ConnectToClusterQuestion.java
@@ -17,17 +17,20 @@
package org.apache.ignite.internal.cli.commands.questions;
+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.core.style.component.QuestionUiComponent.fromYesNoQuestion;
+import static org.apache.ignite.lang.util.StringUtils.nullOrBlank;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.Objects;
+import org.apache.ignite.internal.cli.call.connect.ConnectCallInput;
import org.apache.ignite.internal.cli.call.connect.ConnectSslCall;
import org.apache.ignite.internal.cli.call.connect.SslConfig;
import org.apache.ignite.internal.cli.config.CliConfigKeys;
import org.apache.ignite.internal.cli.config.ConfigManagerProvider;
import org.apache.ignite.internal.cli.config.StateConfigProvider;
-import org.apache.ignite.internal.cli.core.call.UrlCallInput;
import org.apache.ignite.internal.cli.core.flow.builder.FlowBuilder;
import org.apache.ignite.internal.cli.core.flow.builder.Flows;
import org.apache.ignite.internal.cli.core.flow.question.QuestionAskerFactory;
@@ -74,7 +77,8 @@ public class ConnectToClusterQuestion {
UiElements.url(defaultUrl)
);
- return Flows.<Void, UrlCallInput>acceptQuestion(questionUiComponent,
() -> new UrlCallInput(defaultUrl))
+ return Flows.<Void,
ConnectCallInput>acceptQuestion(questionUiComponent,
+ () ->
ConnectCallInput.builder().url(defaultUrl).build())
.then(Flows.fromCall(connectCall))
.print()
.map(ignored -> sessionNodeUrl());
@@ -109,6 +113,29 @@ public class ConnectToClusterQuestion {
return Flows.from(nodeUrl);
}
+ /**
+ * Ask if the user wants to store credentials in config.
+ *
+ * @param username username.
+ * @param password password
+ */
+ public void askQuestionToStoreCredentials(@Nullable String username,
@Nullable String password) {
+ if (!nullOrBlank(username) && !nullOrBlank(password)) {
+ String storedUsername =
configManagerProvider.get().getCurrentProperty(BASIC_AUTHENTICATION_USERNAME.value());
+ String storedPassword =
configManagerProvider.get().getCurrentProperty(BASIC_AUTHENTICATION_PASSWORD.value());
+
+ // Ask question only if cli config has different values.
+ if (!username.equals(storedUsername) ||
!password.equals(storedPassword)) {
+ QuestionUiComponent question = fromYesNoQuestion("Remember
current credentials?");
+ Flows.acceptQuestion(question, () -> {
+
configManagerProvider.get().setProperty(BASIC_AUTHENTICATION_USERNAME.value(),
username);
+
configManagerProvider.get().setProperty(BASIC_AUTHENTICATION_PASSWORD.value(),
password);
+ return "Config saved";
+ }).print().start();
+ }
+ }
+ }
+
/**
* Ask for connect to the cluster and suggest to save the last connected
URL as default.
*/
@@ -136,7 +163,7 @@ public class ConnectToClusterQuestion {
return;
}
- Flows.acceptQuestion(question, () -> new UrlCallInput(clusterUrl))
+ Flows.acceptQuestion(question, () ->
ConnectCallInput.builder().url(clusterUrl).build())
.then(Flows.fromCall(connectCall))
.print()
.ifThen(s -> !Objects.equals(clusterUrl, defaultUrl) &&
session.info() != null,
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/ConfigManager.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/ConfigManager.java
index eb1cddfe92..88acdab07a 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/ConfigManager.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/config/ConfigManager.java
@@ -53,6 +53,10 @@ public interface ConfigManager {
return getConfig(profileName).removeProperty(key);
}
+ default String removeCurrentProperty(String key) {
+ return getConfig(getCurrentProfile().getName()).removeProperty(key);
+ }
+
private Profile getConfig(String profileName) {
return profileName == null ? getCurrentProfile() :
getProfile(profileName);
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/IgniteCliApiExceptionHandler.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/IgniteCliApiExceptionHandler.java
index 96036e08af..b15f7774c1 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/IgniteCliApiExceptionHandler.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/IgniteCliApiExceptionHandler.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.cli.core.exception.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import io.micronaut.http.HttpStatus;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.List;
@@ -69,14 +70,14 @@ public class IgniteCliApiExceptionHandler implements
ExceptionHandler<IgniteCliA
} else if (apiCause != null) {
errorComponentBuilder.header(apiCause.getMessage());
} else {
- Problem problem = extractProblem(cause);
- if (problem.getStatus() == 401) {
+ if (cause.getCode() == HttpStatus.UNAUTHORIZED.getCode()) {
errorComponentBuilder
.header("Authentication error")
.details("Could not connect to node with URL %s. "
- + "Check authentication configuration",
UiElements.url(e.getUrl()))
+ + "Check authentication configuration or
provided username/password", UiElements.url(e.getUrl()))
.verbose(e.getMessage());
} else {
+ Problem problem = extractProblem(cause);
renderProblem(errorComponentBuilder, problem);
}
}
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
index 84ac2e74b6..7e25c11faa 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
@@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import javax.annotation.Nullable;
import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
import org.apache.ignite.internal.cli.event.ConnectionEventListener;
import org.apache.ignite.internal.cli.event.EventPublisher;
@@ -35,6 +34,7 @@ import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.thread.NamedThreadFactory;
import org.apache.ignite.rest.client.api.NodeManagementApi;
import org.apache.ignite.rest.client.invoker.ApiException;
+import org.jetbrains.annotations.Nullable;
/**
* Connection to node heart beat.
diff --git
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/SessionInfo.java
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/SessionInfo.java
index 5ba88806f2..a629bd1bb9 100644
---
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/SessionInfo.java
+++
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/SessionInfo.java
@@ -17,6 +17,8 @@
package org.apache.ignite.internal.cli.core.repl;
+import org.jetbrains.annotations.Nullable;
+
/** Representation of session details. */
public class SessionInfo {
private final String nodeUrl;
@@ -28,7 +30,7 @@ public class SessionInfo {
private final String username;
/** Constructor. */
- private SessionInfo(String nodeUrl, String nodeName, String jdbcUrl,
String username) {
+ private SessionInfo(String nodeUrl, String nodeName, String jdbcUrl,
@Nullable String username) {
this.nodeUrl = nodeUrl;
this.nodeName = nodeName;
this.jdbcUrl = jdbcUrl;
@@ -47,6 +49,7 @@ public class SessionInfo {
return jdbcUrl;
}
+ @Nullable
public String username() {
return username;
}
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 be29530bac..707035f846 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
@@ -37,6 +37,7 @@ import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
@@ -66,6 +67,8 @@ public class ApiClientFactory {
private final Map<ApiClientSettings, ApiClient> clientMap = new
ConcurrentHashMap<>();
+ private final AtomicReference<ApiClientSettings> currentSessionSettings =
new AtomicReference<>();
+
private final ConfigManagerProvider configManagerProvider;
public ApiClientFactory(ConfigManagerProvider configManagerProvider) {
@@ -79,7 +82,17 @@ public class ApiClientFactory {
* @return created API client.
*/
public ApiClient getClient(String path) {
- return getClientFromSettings(settings(path, true));
+ return getClientFromSettings(settingsWithAuth(path));
+ }
+
+ /**
+ * Returns {@link ApiClient} for the base path with basic authentication.
+ *
+ * @param path Base path.
+ * @return created API client.
+ */
+ public ApiClient getClient(String path, String username, String password) {
+ return getClientFromSettings(settingsWithAuth(path, username,
password));
}
/**
@@ -89,7 +102,7 @@ public class ApiClientFactory {
* @return created API client.
*/
public ApiClient getClientWithoutBasicAuthentication(String path) {
- return getClientFromSettings(settings(path, false));
+ return getClientFromSettings(settingsWithoutAuth(path));
}
private ApiClient getClientFromSettings(ApiClientSettings settings) {
@@ -98,27 +111,49 @@ public class ApiClientFactory {
return apiClient;
}
- private ApiClientSettings settings(String path, boolean
enableBasicAuthentication) {
+ private ApiClientSettings settingsWithAuth(String path) {
+ ApiClientSettingsBuilder builder = settingsBuilder(path);
+ return setupAuthentication(builder).build();
+ }
+
+ private ApiClientSettings settingsWithAuth(String path, String username,
String password) {
+ ApiClientSettingsBuilder clientSettingsBuilder = settingsBuilder(path);
+ clientSettingsBuilder.basicAuthenticationUsername(username);
+ clientSettingsBuilder.basicAuthenticationPassword(password);
+ return clientSettingsBuilder.build();
+ }
+
+ private ApiClientSettings settingsWithoutAuth(String path) {
+ ApiClientSettingsBuilder builder = settingsBuilder(path);
+ return builder.build();
+ }
+
+ private ApiClientSettingsBuilder settingsBuilder(String path) {
ConfigManager configManager = configManagerProvider.get();
- ApiClientSettingsBuilder builder = ApiClientSettings.builder()
+ 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()));
-
- if (enableBasicAuthentication) {
- builder
-
.basicAuthenticationUsername(configManager.getCurrentProperty(BASIC_AUTHENTICATION_USERNAME.value()))
-
.basicAuthenticationPassword(configManager.getCurrentProperty(BASIC_AUTHENTICATION_PASSWORD.value()));
- }
-
- return builder.build();
}
- public String basicAuthenticationUsername() {
+ private ApiClientSettingsBuilder
setupAuthentication(ApiClientSettingsBuilder builder) {
ConfigManager configManager = configManagerProvider.get();
- return
configManager.getCurrentProperty(BASIC_AUTHENTICATION_USERNAME.value());
+
+ // Use credentials from current session settings if exist.
+ ApiClientSettings currentCredentialsSettings =
currentSessionSettings();
+ String username = currentCredentialsSettings != null
+ ? currentCredentialsSettings.basicAuthenticationUsername()
+ :
configManager.getCurrentProperty(BASIC_AUTHENTICATION_USERNAME.value());
+ String password = currentCredentialsSettings != null
+ ? currentCredentialsSettings.basicAuthenticationPassword()
+ :
configManager.getCurrentProperty(BASIC_AUTHENTICATION_PASSWORD.value());
+ builder
+ .basicAuthenticationUsername(username)
+ .basicAuthenticationPassword(password);
+
+ return builder;
}
/**
@@ -150,6 +185,23 @@ public class ApiClientFactory {
}
}
+ /**
+ * Set api client settings for current session.
+ *
+ * @param settings api client settings
+ */
+ public void setSessionSettings(@Nullable ApiClientSettings settings) {
+ if (settings != null) {
+ currentSessionSettings.compareAndSet(null, settings);
+ } else {
+ currentSessionSettings.set(null);
+ }
+ }
+
+ public ApiClientSettings currentSessionSettings() {
+ return currentSessionSettings.get();
+ }
+
private static Builder applySslSettings(Builder builder, ApiClientSettings
settings) throws UnrecoverableKeyException,
CertificateException,
NoSuchAlgorithmException,
diff --git
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommandTest.java
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommandTest.java
index 615296b195..bee56dfc31 100644
---
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommandTest.java
+++
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/connect/ConnectCommandTest.java
@@ -41,4 +41,52 @@ class ConnectCommandTest extends CliCommandTestBase {
() -> assertErrOutputContains("Node nodeName not found.
Provide valid name or use URL")
);
}
+
+ @Test
+ @DisplayName("Should throw error if only username provided")
+ void usernameOnly() {
+ execute("http://localhost:1111 --username user ");
+
+ assertAll(
+ () -> assertExitCodeIs(2),
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputContains("Error: Missing required
argument(s): --password=<password>")
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw error if only short username provided")
+ void shortUsernameOnly() {
+ execute("http://localhost:1111 -u user ");
+
+ assertAll(
+ () -> assertExitCodeIs(2),
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputContains("Error: Missing required
argument(s): --password=<password>")
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw error if only password provided")
+ void passwordOnly() {
+ execute("http://localhost:1111 --password password");
+
+ assertAll(
+ () -> assertExitCodeIs(2),
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputContains("Error: Missing required
argument(s): --username=<username>")
+ );
+ }
+
+ @Test
+ @DisplayName("Should throw error if only short password provided")
+ void shortPasswordOnly() {
+ execute("http://localhost:1111 -p password");
+
+ assertAll(
+ () -> assertExitCodeIs(2),
+ this::assertOutputIsEmpty,
+ () -> assertErrOutputContains("Error: Missing required
argument(s): --username=<username>")
+ );
+ }
}