From b4f91018024e043c451c8f36d64ce295e9d77787 Mon Sep 17 00:00:00 2001
From: Dmitry Ivanov <ivadmi5@gmail.com>
Date: Mon, 31 Jul 2023 13:51:23 +0300
Subject: [PATCH] feat: Add `-a,--auth-type` to `picodata connect`

NB: this commit needs the fixes in tarantool-sys,
so we have to update the git submodule.
---
 src/args.rs                  | 13 +++++++-
 src/luamod.lua               | 16 +++++++---
 src/main.rs                  |  4 +--
 src/schema.rs                |  1 +
 tarantool-sys                |  2 +-
 test/int/test_cli_connect.py | 61 ++++++++++++++++++++++++++++++++++++
 6 files changed, 89 insertions(+), 8 deletions(-)

diff --git a/src/args.rs b/src/args.rs
index a4db68e747..20b8b0d68c 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -11,6 +11,7 @@ use thiserror::Error;
 use crate::failure_domain::FailureDomain;
 use crate::instance::InstanceId;
 use crate::replicaset::ReplicasetId;
+use crate::schema::AuthMethod;
 use crate::util::Uppercase;
 
 #[derive(Debug, Parser)]
@@ -316,13 +317,23 @@ pub struct Connect {
     #[clap(
         short = 'u',
         long = "user",
-        value_name = "user",
+        value_name = "USER",
         default_value = "guest",
         env = "PICODATA_USER"
     )]
     /// The username to connect with. Ignored if provided in <ADDRESS>.
     pub user: String,
 
+    #[clap(
+        short = 'a',
+        long = "auth-type",
+        value_name = "METHOD",
+        default_value = AuthMethod::ChapSha1.as_str(),
+        arg_enum,
+    )]
+    /// The preferred authentication method.
+    pub auth_method: AuthMethod,
+
     #[clap(value_name = "ADDRESS")]
     /// Picodata instance address. Format: `[user@][host][:port]`
     pub address: Address,
diff --git a/src/luamod.lua b/src/luamod.lua
index 3c2f6eb940..f7237aedb3 100644
--- a/src/luamod.lua
+++ b/src/luamod.lua
@@ -191,6 +191,7 @@ Params:
     2. password (string)
     3. opts (table)
         - timeout (number), seconds
+        - auth_type (string)
 
 Returns:
 
@@ -203,7 +204,10 @@ function pico.create_user(user, password, opts)
         box.internal.check_param(user, 'user', 'string')
         box.internal.check_param(password, 'password', 'string')
         -- TODO: check password requirements.
-        box.internal.check_param_table(opts, { timeout = 'number' })
+        box.internal.check_param_table(opts, {
+            timeout = 'number',
+            auth_type = 'string',
+        })
         opts = opts or {}
         if not opts.timeout then
             box.error(box.error.ILLEGAL_PARAMS, 'opts.timeout is mandatory')
@@ -217,7 +221,7 @@ function pico.create_user(user, password, opts)
 
     -- XXX: we construct this closure every time the function is called,
     -- which is bad for performance/jit. Refactor if problems are discovered.
-    local auth_type = box.cfg.auth_type
+    local auth_type = opts.auth_type or box.cfg.auth_type
     local auth_data = box.internal.prepare_auth(auth_type, password, user)
     local function make_op_if_needed()
         local grantee_def = box.space._user.index.name:get(user)
@@ -270,6 +274,7 @@ Params:
     2. password (string)
     3. opts (table)
         - timeout (number), seconds
+        - auth_type (string)
 
 Returns:
 
@@ -281,7 +286,10 @@ function pico.change_password(user, password, opts)
     local ok, err = pcall(function()
         box.internal.check_param(user, 'user', 'string')
         box.internal.check_param(password, 'password', 'string')
-        box.internal.check_param_table(opts, { timeout = 'number' })
+        box.internal.check_param_table(opts, {
+            timeout = 'number',
+            auth_type = 'string',
+        })
         opts = opts or {}
         if not opts.timeout then
             box.error(box.error.ILLEGAL_PARAMS, 'opts.timeout is mandatory')
@@ -297,7 +305,7 @@ function pico.change_password(user, password, opts)
 
     -- XXX: we construct this closure every time the function is called,
     -- which is bad for performance/jit. Refactor if problems are discovered.
-    local auth_type = box.cfg.auth_type
+    local auth_type = opts.auth_type or box.cfg.auth_type
     local auth_data = box.internal.prepare_auth(auth_type, password, user)
     local function make_op_if_needed()
         -- TODO: allow `user` to be a user id instead of name
diff --git a/src/main.rs b/src/main.rs
index adcfc12c1c..191e8baf57 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -254,8 +254,8 @@ fn main_connect(args: args::Connect) -> ! {
     };
 
     let address = format!(
-        "{user}:{password}@{}:{}",
-        args.address.host, args.address.port
+        "{user}:{password}@{}:{}?auth_type={}",
+        args.address.host, args.address.port, args.auth_method
     );
 
     let rc = tarantool_main!(
diff --git a/src/schema.rs b/src/schema.rs
index dfaa165c79..ba5c307d5f 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -196,6 +196,7 @@ pub struct AuthDef {
 }
 
 ::tarantool::define_str_enum! {
+    #[derive(clap::ArgEnum)]
     pub enum AuthMethod {
         ChapSha1 = "chap-sha1",
         MD5 = "md5",
diff --git a/tarantool-sys b/tarantool-sys
index 4056a2006d..1bb6aae1f5 160000
--- a/tarantool-sys
+++ b/tarantool-sys
@@ -1 +1 @@
-Subproject commit 4056a2006d93e2cd8a96a52b689deacab640d48e
+Subproject commit 1bb6aae1f575f0144f2b89b0a86bd188edaac13b
diff --git a/test/int/test_cli_connect.py b/test/int/test_cli_connect.py
index 411c822c67..ace2045173 100644
--- a/test/int/test_cli_connect.py
+++ b/test/int/test_cli_connect.py
@@ -162,3 +162,64 @@ def test_connection_refused(binary_path: str):
 
     cli.expect_exact("Connection is not established")
     cli.expect_exact(pexpect.EOF)
+
+
+def test_connect_auth_type_ok(i1: Instance):
+    cli = pexpect.spawn(
+        command=i1.binary_path,
+        args=["connect", f"{i1.host}:{i1.port}", "-u", "testuser", "-a", "chap-sha1"],
+        encoding="utf-8",
+        timeout=1,
+    )
+    cli.logfile = sys.stdout
+
+    cli.expect_exact("Enter password for testuser: ")
+    cli.sendline("testpass")
+
+    cli.expect_exact(f"connected to {i1.host}:{i1.port}")
+    cli.expect_exact(f"{i1.host}:{i1.port}>")
+
+    cli.sendline("box.session.user()")
+    cli.expect_exact("---\r\n")
+    cli.expect_exact("- testuser\r\n")
+    cli.expect_exact("...\r\n")
+    cli.expect_exact("\r\n")
+
+    eprint("^D")
+    cli.sendcontrol("d")
+    cli.expect_exact(pexpect.EOF)
+
+
+def test_connect_auth_type_different(i1: Instance):
+    cli = pexpect.spawn(
+        command=i1.binary_path,
+        args=["connect", f"{i1.host}:{i1.port}", "-u", "testuser", "-a", "chap-sha1"],
+        encoding="utf-8",
+        timeout=1,
+    )
+    cli.logfile = sys.stdout
+
+    cli.expect_exact("Enter password for testuser: ")
+    cli.sendline("")
+
+    cli.expect_exact("Connection is not established")
+    cli.expect_exact(pexpect.EOF)
+
+
+def test_connect_auth_type_unknown(binary_path: str):
+    cli = pexpect.spawn(
+        command=binary_path,
+        args=["connect", ":0", "-u", "testuser", "-a", "deadbeef"],
+        env={"NO_COLOR": "1"},
+        encoding="utf-8",
+        timeout=1,
+    )
+    cli.logfile = sys.stdout
+
+    cli.expect_exact(
+        'error: Invalid value "deadbeef" '
+        + "for '--auth-type <METHOD>': "
+        + 'unknown AuthMethod "deadbeef"\r\n\r\n'
+        + "For more information try --help\r\n"
+    )
+    cli.expect_exact(pexpect.EOF)
-- 
GitLab