Automatically login & reauth (#1691)

* Automatically login & reauth
* Revert automatical format by vscode
* Only ask user to choose username once
* Use dialoguer::Confirm; fix logic
This commit is contained in:
Yuxuan Lu 2023-06-05 20:03:52 +08:00 committed by GitHub
parent 5ecc5dffa0
commit f4b355c299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 24 deletions

12
Cargo.lock generated
View file

@ -316,6 +316,17 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "async-recursion"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.17",
]
[[package]] [[package]]
name = "async-session" name = "async-session"
version = "2.0.1" version = "2.0.1"
@ -2388,6 +2399,7 @@ dependencies = [
name = "kanidm_tools" name = "kanidm_tools"
version = "1.1.0-beta.13-dev" version = "1.1.0-beta.13-dev"
dependencies = [ dependencies = [
"async-recursion",
"clap", "clap",
"clap_complete", "clap_complete",
"compact_jwt", "compact_jwt",

View file

@ -38,6 +38,7 @@ homepage = "https://github.com/kanidm/kanidm/"
repository = "https://github.com/kanidm/kanidm/" repository = "https://github.com/kanidm/kanidm/"
[workspace.dependencies] [workspace.dependencies]
async-recursion = "1.0.4"
async-trait = "^0.1.68" async-trait = "^0.1.68"
base32 = "^0.4.0" base32 = "^0.4.0"
base64 = "^0.21.0" base64 = "^0.21.0"

View file

@ -30,6 +30,7 @@ name = "kanidm_ssh_authorizedkeys_direct"
path = "src/ssh_authorizedkeys.rs" path = "src/ssh_authorizedkeys.rs"
[dependencies] [dependencies]
async-recursion.workplace = true
clap = { workspace = true, features = ["derive", "env"] } clap = { workspace = true, features = ["derive", "env"] }
compact_jwt = { workspace = true, features = ["openssl"] } compact_jwt = { workspace = true, features = ["openssl"] }
dialoguer.workspace = true dialoguer.workspace = true

View file

@ -1,20 +1,30 @@
use std::env;
use std::str::FromStr; use std::str::FromStr;
use async_recursion::async_recursion;
use compact_jwt::{Jws, JwsUnverified}; use compact_jwt::{Jws, JwsUnverified};
use dialoguer::theme::ColorfulTheme; use dialoguer::theme::ColorfulTheme;
use dialoguer::Select; use dialoguer::{Confirm, Select};
use kanidm_client::{KanidmClient, KanidmClientBuilder}; use kanidm_client::{KanidmClient, KanidmClientBuilder};
use kanidm_proto::constants::{DEFAULT_CLIENT_CONFIG_PATH, DEFAULT_CLIENT_CONFIG_PATH_HOME}; use kanidm_proto::constants::{DEFAULT_CLIENT_CONFIG_PATH, DEFAULT_CLIENT_CONFIG_PATH_HOME};
use kanidm_proto::v1::UserAuthToken; use kanidm_proto::v1::UserAuthToken;
use crate::session::read_tokens; use crate::session::read_tokens;
use crate::CommonOpt; use crate::{CommonOpt, LoginOpt, ReauthOpt};
#[derive(Clone)]
pub enum OpType { pub enum OpType {
Read, Read,
Write, Write,
} }
#[derive(Debug)]
pub enum ToClientError {
NeedLogin(String),
NeedReauth(String),
Other,
}
impl CommonOpt { impl CommonOpt {
pub fn to_unauth_client(&self) -> KanidmClient { pub fn to_unauth_client(&self) -> KanidmClient {
let config_path: String = shellexpand::tilde(DEFAULT_CLIENT_CONFIG_PATH_HOME).into_owned(); let config_path: String = shellexpand::tilde(DEFAULT_CLIENT_CONFIG_PATH_HOME).into_owned();
@ -73,14 +83,14 @@ impl CommonOpt {
}) })
} }
pub async fn to_client(&self, optype: OpType) -> KanidmClient { async fn try_to_client(&self, optype: OpType) -> Result<KanidmClient, ToClientError> {
let client = self.to_unauth_client(); let client = self.to_unauth_client();
// Read the token file. // Read the token file.
let tokens = match read_tokens() { let tokens = match read_tokens() {
Ok(t) => t, Ok(t) => t,
Err(_e) => { Err(_e) => {
error!("Error retrieving authentication token store"); error!("Error retrieving authentication token store");
std::process::exit(1); return Err(ToClientError::Other);
} }
}; };
@ -88,18 +98,22 @@ impl CommonOpt {
error!( error!(
"No valid authentication tokens found. Please login with the 'login' subcommand." "No valid authentication tokens found. Please login with the 'login' subcommand."
); );
std::process::exit(1); return Err(ToClientError::Other);
} }
// we need to store guessed username for login and reauth.
let username;
// If we have a username, use that to select tokens // If we have a username, use that to select tokens
let token = match &self.username { let token = match &self.username {
Some(username) => { Some(_username) => {
username = _username.clone();
// Is it in the store? // Is it in the store?
match tokens.get(username) { match tokens.get(&username) {
Some(t) => t.clone(), Some(t) => t.clone(),
None => { None => {
error!("No valid authentication tokens found for {}.", username); error!("No valid authentication tokens found for {}.", username);
std::process::exit(1); return Err(ToClientError::NeedLogin(username));
} }
} }
} }
@ -109,12 +123,16 @@ impl CommonOpt {
let (f_uname, f_token) = tokens.iter().next().expect("Memory Corruption"); let (f_uname, f_token) = tokens.iter().next().expect("Memory Corruption");
// else pick the first token // else pick the first token
debug!("Using cached token for name {}", f_uname); debug!("Using cached token for name {}", f_uname);
username = f_uname.clone();
f_token.clone() f_token.clone()
} else { } else {
// Unable to automatically select the user because multiple tokens exist // Unable to automatically select the user because multiple tokens exist
// so we'll prompt the user to select one // so we'll prompt the user to select one
match prompt_for_username_get_token() { match prompt_for_username_get_values() {
Ok(value) => value, Ok((f_uname, f_token)) => {
username = f_uname;
f_token
}
Err(msg) => { Err(msg) => {
error!("{}", msg); error!("{}", msg);
std::process::exit(1); std::process::exit(1);
@ -128,7 +146,7 @@ impl CommonOpt {
Ok(jwtu) => jwtu, Ok(jwtu) => jwtu,
Err(e) => { Err(e) => {
error!("Unable to parse token - {:?}", e); error!("Unable to parse token - {:?}", e);
std::process::exit(1); return Err(ToClientError::Other);
} }
}; };
@ -145,7 +163,7 @@ impl CommonOpt {
"Session has expired for {} - you may need to login again.", "Session has expired for {} - you may need to login again.",
uat.spn uat.spn
); );
std::process::exit(1); return Err(ToClientError::NeedLogin(username));
} }
} }
@ -158,7 +176,7 @@ impl CommonOpt {
"Privileges have expired for {} - you need to re-authenticate again.", "Privileges have expired for {} - you need to re-authenticate again.",
uat.spn uat.spn
); );
std::process::exit(1); return Err(ToClientError::NeedReauth(username));
} }
} }
} }
@ -166,14 +184,67 @@ impl CommonOpt {
Err(e) => { Err(e) => {
error!("Unable to read token for requested user - you may need to login again."); error!("Unable to read token for requested user - you may need to login again.");
debug!(?e, "JWT Error"); debug!(?e, "JWT Error");
std::process::exit(1); return Err(ToClientError::NeedLogin(username));
} }
}; };
// Set it into the client // Set it into the client
client.set_token(token).await; client.set_token(token).await;
client Ok(client)
}
#[async_recursion]
pub async fn to_client(&self, optype: OpType) -> KanidmClient {
match self.try_to_client(optype.clone()).await {
Ok(c) => c,
Err(e) => {
match e {
ToClientError::NeedLogin(username) => {
if !Confirm::new()
.with_prompt("Would you like to login again?")
.interact()
.expect("Failed to interact with interactive session")
{
std::process::exit(1);
}
let mut copt = self.clone();
copt.username = Some(username);
let login_opt = LoginOpt {
copt,
password: env::var("KANIDM_PASSWORD").ok(),
};
login_opt.exec().await;
// we still use `to_client` instead of `try_to_client` because we may need to prompt user to re-auth again.
// since reauth_opt will call `to_client`, this function is recursive anyway.
// we use copt since it's username is updated.
return login_opt.copt.to_client(optype).await;
}
ToClientError::NeedReauth(username) => {
if !Confirm::new()
.with_prompt("Would you like to re-authenticate?")
.interact()
.expect("Failed to interact with interactive session")
{
std::process::exit(1);
}
let mut copt = self.clone();
copt.username = Some(username);
let reauth_opt = ReauthOpt { copt };
// calls `to_client` recursively
// but should not goes into `NeedLogin` branch again
reauth_opt.exec().await;
if let Ok(c) = reauth_opt.copt.try_to_client(optype).await {
return c;
}
}
ToClientError::Other => {
std::process::exit(1);
}
}
std::process::exit(1);
}
}
} }
} }

View file

@ -14,7 +14,7 @@ pub struct DebugOpt {
pub debug: bool, pub debug: bool,
} }
#[derive(Debug, Args)] #[derive(Debug, Args, Clone)]
pub struct CommonOpt { pub struct CommonOpt {
/// Enable debbuging of the kanidm tool /// Enable debbuging of the kanidm tool
#[clap(short, long, env = "KANIDM_DEBUG")] #[clap(short, long, env = "KANIDM_DEBUG")]
@ -29,14 +29,14 @@ pub struct CommonOpt {
#[clap(parse(from_os_str), short = 'C', long = "ca", env = "KANIDM_CA_PATH")] #[clap(parse(from_os_str), short = 'C', long = "ca", env = "KANIDM_CA_PATH")]
pub ca_path: Option<PathBuf>, pub ca_path: Option<PathBuf>,
/// Log format (still in very early development) /// Log format (still in very early development)
#[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value="text")] #[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value = "text")]
output_mode: String, output_mode: String,
} }
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct GroupNamedMembers { pub struct GroupNamedMembers {
name: String, name: String,
#[clap(required=true,min_values=1)] #[clap(required = true, min_values = 1)]
members: Vec<String>, members: Vec<String>,
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
@ -503,7 +503,7 @@ pub enum RecycleOpt {
pub struct LoginOpt { pub struct LoginOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
#[clap(short, long, env="KANIDM_PASSWORD", hide=true)] #[clap(short, long, env = "KANIDM_PASSWORD", hide = true)]
/// Supply a password to the login option /// Supply a password to the login option
password: Option<String>, password: Option<String>,
} }
@ -711,7 +711,6 @@ pub struct OptSetDomainDisplayName {
new_display_name: String, new_display_name: String,
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum PwBadlistOpt { pub enum PwBadlistOpt {
#[clap[name = "show"]] #[clap[name = "show"]]
@ -724,7 +723,7 @@ pub enum PwBadlistOpt {
Upload { Upload {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
#[clap(parse(from_os_str),required=true,min_values=1)] #[clap(parse(from_os_str), required = true, min_values = 1)]
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
/// Perform a dry run and display the list that would have been uploaded instead. /// Perform a dry run and display the list that would have been uploaded instead.
#[clap(short = 'n', long)] #[clap(short = 'n', long)]
@ -736,9 +735,9 @@ pub enum PwBadlistOpt {
Remove { Remove {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
#[clap(parse(from_os_str), required=true, min_values=1)] #[clap(parse(from_os_str), required = true, min_values = 1)]
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
} },
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
@ -863,7 +862,7 @@ pub enum SystemOpt {
Synch { Synch {
#[clap(subcommand)] #[clap(subcommand)]
commands: SynchOpt, commands: SynchOpt,
} },
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]