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

Reply via email to