diff --git a/Cargo.lock b/Cargo.lock index 70b2053a3c..7305ec44ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,9 +529,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.2" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" dependencies = [ "bytes", "memchr", @@ -1466,7 +1466,7 @@ dependencies = [ "strip-ansi-escapes", "target-spec", "toml", - "toml_edit", + "toml_edit 0.10.1", "twox-hash", ] @@ -1536,6 +1536,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi", +] + [[package]] name = "humansize" version = "1.1.0" @@ -2127,6 +2136,7 @@ dependencies = [ "colored", "datatest-stable", "difference", + "home", "include_dir", "itertools 0.10.1", "move-binary-format", @@ -2156,6 +2166,7 @@ dependencies = [ "serde 1.0.130", "serde_yaml", "tempfile", + "toml_edit 0.14.4", "walkdir", "workspace-hack", ] @@ -4778,6 +4789,18 @@ dependencies = [ "kstring", ] +[[package]] +name = "toml_edit" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5376256e44f2443f8896ac012507c19a012df0fe8758b55246ae51a2279db51f" +dependencies = [ + "combine", + "indexmap", + "itertools 0.10.1", + "serde 1.0.130", +] + [[package]] name = "tracing" version = "0.1.26" @@ -4868,7 +4891,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee73e6e4924fe940354b8d4d98cad5231175d615cd855b758adc658c0aac6a0" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "static_assertions", ] diff --git a/language/tools/move-cli/Cargo.toml b/language/tools/move-cli/Cargo.toml index f9f50ede34..d658ba65e4 100644 --- a/language/tools/move-cli/Cargo.toml +++ b/language/tools/move-cli/Cargo.toml @@ -22,6 +22,7 @@ tempfile = "3.2.0" walkdir = "2.3.1" codespan-reporting = "0.11.1" itertools = "0.10.0" +toml_edit = { version = "0.14.3", features = ["easy"] } bcs = "0.1.2" move-bytecode-verifier = { path = "../../move-bytecode-verifier" } @@ -52,6 +53,7 @@ workspace-hack = { version = "0.1", path = "../../../crates/workspace-hack" } [dev-dependencies] datatest-stable = "0.1.1" +home = "0.5.3" [[bin]] name = "move" diff --git a/language/tools/move-cli/src/lib.rs b/language/tools/move-cli/src/lib.rs index 1e0d763187..528c2c0ef5 100644 --- a/language/tools/move-cli/src/lib.rs +++ b/language/tools/move-cli/src/lib.rs @@ -7,6 +7,7 @@ pub mod base; pub mod experimental; pub mod package; pub mod sandbox; +mod login; /// Default directory where saved Move resources live pub const DEFAULT_STORAGE_DIR: &str = "storage"; @@ -95,6 +96,8 @@ pub enum Command { #[clap(subcommand)] cmd: experimental::cli::ExperimentalCommand, }, + #[clap(name = "login")] + Login, } pub fn run_cli( @@ -119,6 +122,7 @@ pub fn run_cli( cmd, natives, ), + Command::Login => login::cli::handle_login_commands(move_args.build_config.clone()), } } diff --git a/language/tools/move-cli/src/login/cli.rs b/language/tools/move-cli/src/login/cli.rs new file mode 100644 index 0000000000..361ccbbcff --- /dev/null +++ b/language/tools/move-cli/src/login/cli.rs @@ -0,0 +1,237 @@ +use anyhow::{bail, Result}; +use std::fs::File; +use std::path::PathBuf; +use std::{fs, io}; +use toml_edit::easy::map::Map; +use toml_edit::easy::Value; + +pub fn handle_login_commands(config: move_package::BuildConfig) -> Result<()> { + let url: &str; + if cfg!(debug_assertions) { + url = "https://movey-app-staging.herokuapp.com"; + } else { + url = "https://movey.net"; + } + println!( + "Please paste the API Token found on {}/settings/tokens below", + url + ); + let mut line = String::new(); + loop { + match io::stdin().read_line(&mut line) { + Ok(_) => { + if let Some('\n') = line.chars().next_back() { + line.pop(); + } + if let Some('\r') = line.chars().next_back() { + line.pop(); + } + if line.len() != 0 { + break; + } + println!("Invalid API Token. Try again!"); + } + Err(err) => { + bail!("Error reading file: {}", err); + } + } + } + save_credential(line, config.test_mode)?; + println!("Token for Movey saved."); + Ok(()) +} + +pub fn save_credential(token: String, is_test_mode: bool) -> Result<()> { + let mut move_home = std::env::var("MOVE_HOME").unwrap_or_else(|_| { + format!( + "{}/.move", + std::env::var("HOME").expect("env var 'HOME' must be set") + ) + }); + if is_test_mode { + move_home.push_str("/test") + } + fs::create_dir_all(&move_home)?; + let credential_path = move_home + "/credential.toml"; + let credential_file = PathBuf::from(&credential_path.clone()); + if !credential_file.exists() { + File::create(&credential_path)?; + } + + let old_contents: String; + match fs::read_to_string(&credential_path) { + Ok(contents) => { + old_contents = contents; + } + Err(error) => bail!("Error reading input: {}", error), + } + let mut toml: Value = old_contents + .parse() + .map_err(|e| anyhow::Error::from(e).context("could not parse input as TOML"))?; + + if let Some(registry) = toml.as_table_mut().unwrap().get_mut("registry") { + if let Some(toml_token) = registry.as_table_mut().unwrap().get_mut("token") { + *toml_token = Value::String(token); + } else { + registry + .as_table_mut() + .unwrap() + .insert(String::from("token"), Value::String(token)); + } + } else { + let mut value = Map::new(); + value.insert(String::from("token"), Value::String(token)); + toml.as_table_mut() + .unwrap() + .insert(String::from("registry"), Value::Table(value)); + } + + let new_contents = toml.to_string(); + fs::write(credential_file, new_contents).expect("Unable to write file"); + let file = File::open(&credential_path)?; + set_permissions(&file, 0o600)?; + Ok(()) +} + +#[cfg(unix)] +fn set_permissions(file: &File, mode: u32) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mut perms = file.metadata()?.permissions(); + perms.set_mode(mode); + file.set_permissions(perms)?; + Ok(()) +} + +#[cfg(not(unix))] +#[allow(unused)] +fn set_permissions(file: &File, mode: u32) -> Result<()> { + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use home::home_dir; + use std::env; + + fn setup_move_home() -> (String, String) { + let mut move_home = env::var("MOVE_HOME").unwrap_or_else(|_| { + env::var("HOME").unwrap_or_else(|_| { + let home_dir = home_dir().unwrap().to_string_lossy().to_string(); + env::set_var("HOME", &home_dir); + home_dir + }) + }); + move_home.push_str("/.move/test"); + let credential_path = move_home.clone() + "/credential.toml"; + return (move_home, credential_path); + } + + fn clean_up() { + let (move_home, _) = setup_move_home(); + let _ = fs::remove_dir_all(move_home); + } + + #[test] + fn save_credential_works_if_no_credential_file_exists() { + let (move_home, credential_path) = setup_move_home(); + let _ = fs::remove_dir_all(&move_home); + + save_credential(String::from("test_token"), true).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + + clean_up(); + } + + #[test] + fn save_credential_works_if_empty_credential_file_exists() { + let (move_home, credential_path) = setup_move_home(); + + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + File::create(&credential_path).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + assert!(toml.as_table_mut().unwrap().get_mut("registry").is_none()); + + save_credential(String::from("test_token"), true).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + + clean_up(); + } + + #[test] + fn save_credential_works_if_token_field_exists() { + let (move_home, credential_path) = setup_move_home(); + + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + File::create(&credential_path).unwrap(); + + let old_content = + String::from("[registry]\ntoken = \"old_test_token\"\nversion = \"0.0.0\"\n"); + fs::write(&credential_path, old_content).expect("Unable to write file"); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("old_test_token")); + assert!(!token.to_string().contains("new_world")); + + save_credential(String::from("new_world"), true).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("new_world")); + assert!(!token.to_string().contains("old_test_token")); + let version = registry.as_table_mut().unwrap().get_mut("version").unwrap(); + assert!(version.to_string().contains("0.0.0")); + + clean_up(); + } + + #[test] + fn save_credential_works_if_empty_token_field_exists() { + let (move_home, credential_path) = setup_move_home(); + + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + File::create(&credential_path).unwrap(); + + let old_content = String::from("[registry]\ntoken = \"\"\nversion = \"0.0.0\"\n"); + fs::write(&credential_path, old_content).expect("Unable to write file"); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(!token.to_string().contains("test_token")); + + save_credential(String::from("test_token"), true).unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + let version = registry.as_table_mut().unwrap().get_mut("version").unwrap(); + assert!(version.to_string().contains("0.0.0")); + + clean_up(); + } +} diff --git a/language/tools/move-cli/src/login/mod.rs b/language/tools/move-cli/src/login/mod.rs new file mode 100644 index 0000000000..4f773726a2 --- /dev/null +++ b/language/tools/move-cli/src/login/mod.rs @@ -0,0 +1 @@ +pub mod cli; diff --git a/language/tools/move-cli/tests/cli_tests.rs b/language/tools/move-cli/tests/cli_tests.rs index 5b82ce391f..93141bc641 100644 --- a/language/tools/move-cli/tests/cli_tests.rs +++ b/language/tools/move-cli/tests/cli_tests.rs @@ -2,8 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use move_cli::sandbox::commands::test; +#[cfg(unix)] +use std::fs::File; +use std::io::Write; +use std::{env, fs}; +use home::home_dir; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; +use std::process::Stdio; +use toml_edit::easy::Value; pub const CLI_METATEST_PATH: [&str; 3] = ["tests", "metatests", "args.txt"]; @@ -58,3 +67,116 @@ fn cross_process_locking_git_deps() { .expect("Package2 failed"); handle.join().unwrap(); } + +#[test] +fn save_credential_works() { + #[cfg(debug_assertions)] + const CLI_EXE: &str = "../../../target/debug/move"; + #[cfg(not(debug_assertions))] + const CLI_EXE: &str = "../../../target/release/move"; + let (move_home, credential_path) = setup_move_home(); + assert!(fs::read_to_string(&credential_path).is_err()); + + match std::process::Command::new(CLI_EXE) + .current_dir(".") + .args(["login"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + { + Ok(child) => { + let token = "test_token"; + child + .stdin + .as_ref() + .unwrap() + .write(token.as_bytes()) + .unwrap(); + match child.wait_with_output() { + Ok(output) => { + assert!(String::from_utf8_lossy(&output.stdout).contains( + "Please paste the API Token found on \ + https://movey-app-staging.herokuapp.com/settings/tokens below" + )); + Ok(()) + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + .unwrap(); + + let contents = fs::read_to_string(&credential_path).expect("Unable to read file"); + let mut toml: Value = contents.parse().unwrap(); + let registry = toml.as_table_mut().unwrap().get_mut("registry").unwrap(); + let token = registry.as_table_mut().unwrap().get_mut("token").unwrap(); + assert!(token.to_string().contains("test_token")); + + clean_up(&move_home) +} + +#[cfg(unix)] +#[test] +fn save_credential_fails_if_undeletable_credential_file_exists() { + #[cfg(debug_assertions)] + const CLI_EXE: &str = "../../../target/debug/move"; + #[cfg(not(debug_assertions))] + const CLI_EXE: &str = "../../../target/release/move"; + let (move_home, credential_path) = setup_move_home(); + let file = File::create(&credential_path).unwrap(); + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o000); + file.set_permissions(perms).unwrap(); + + match std::process::Command::new(CLI_EXE) + .current_dir(".") + .args(["login"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() { + Ok(child) => { + let token = "test_token"; + child.stdin.as_ref().unwrap().write(token.as_bytes()).unwrap(); + match child.wait_with_output() { + Ok(output) => { + assert!(String::from_utf8_lossy(&output.stdout) + .contains( + "Please paste the API Token found on \ + https://movey-app-staging.herokuapp.com/settings/tokens below" + ) + ); + assert!(String::from_utf8_lossy(&output.stderr) + .contains( + "Error: Error reading input: Permission denied (os error 13)" + ) + ); + Ok(()) + }, + Err(error) => Err(error), + } + } + Err(error) => Err(error), + }.unwrap(); + + let mut perms = file.metadata().unwrap().permissions(); + perms.set_mode(0o600); + file.set_permissions(perms).unwrap(); + let _ = fs::remove_file(&credential_path); + + clean_up(&move_home) +} + +fn setup_move_home() -> (String, String) { + let move_home = home_dir().unwrap().to_string_lossy().to_string() + "/.move/test"; + env::set_var("MOVE_HOME", &move_home); + let _ = fs::remove_dir_all(&move_home); + fs::create_dir_all(&move_home).unwrap(); + let credential_path = move_home.clone() + "/credential.toml"; + return (move_home, credential_path); +} + +fn clean_up(move_home: &str) { + let _ = fs::remove_dir_all(move_home); +}