From cb6ee2c66bab7cb1a8f1124b4469187db8dc207b Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sat, 24 May 2025 11:25:16 +0200 Subject: [PATCH 01/12] Update user-config command with new options --- src/commands/config.rs | 232 +++++++++++++++++++++++++++++++++++++---- src/models/config.rs | 8 +- 2 files changed, 219 insertions(+), 21 deletions(-) diff --git a/src/commands/config.rs b/src/commands/config.rs index 435701b..93fe2e8 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -46,6 +46,18 @@ pub enum ConfigCommand { interval_seconds: i32, }, + /// Sets the max number of invited sessions by user + SetMaxInvites { + /// Max number of invited sessions by user + invite_limit: i32, + }, + + /// Sets the option to preserve original files, before encoding, as an attachment + SetPreserveOriginals { + /// Preserve original media, before encoding, as an attachment? + preserve_originals: bool, + }, + /// Sets custom CSS for the vault SetCSS { /// Path to the css file to use @@ -100,6 +112,12 @@ pub async fn run_config_cmd(global_opts: CommandGlobalOptions, cmd: ConfigComman ConfigCommand::SetVideoPreviewsInterval { interval_seconds } => { run_cmd_config_set_video_previews_interval(global_opts, interval_seconds).await; } + ConfigCommand::SetMaxInvites { invite_limit } => { + run_cmd_config_set_invite_limit(global_opts, invite_limit).await; + } + ConfigCommand::SetPreserveOriginals { preserve_originals } => { + run_cmd_config_set_preserve_originals(global_opts, preserve_originals).await; + } ConfigCommand::SetCSS { file_path } => { run_cmd_config_set_css(global_opts, file_path).await; } @@ -185,9 +203,24 @@ pub async fn run_cmd_config_get(global_opts: CommandGlobalOptions) { res_video_previews_interval = 3; } + let mut invite_limit = config.invite_limit; + + if invite_limit == 0 { + invite_limit = 10; + } + println!("Max tasks in parallel: {res_max_tasks}"); println!("Number of encoding threads: {res_encoding_threads}"); println!("Video previews interval: {res_video_previews_interval} seconds"); + println!("Max number of invited sessions by user: {invite_limit}"); + println!( + "Preserve original files, before encoding, as an attachment?: {}", + if config.preserve_originals { + "Yes" + } else { + "No" + } + ); if !config.resolutions.is_empty() { let list: Vec = config @@ -354,8 +387,7 @@ pub async fn run_cmd_config_set_title(global_opts: CommandGlobalOptions, title: // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -436,8 +468,7 @@ pub async fn run_cmd_config_set_max_tasks(global_opts: CommandGlobalOptions, max // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -521,8 +552,7 @@ pub async fn run_cmd_config_set_encoding_threads( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -606,8 +636,7 @@ pub async fn run_cmd_config_set_video_previews_interval( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -637,6 +666,175 @@ pub async fn run_cmd_config_set_video_previews_interval( } } +pub async fn run_cmd_config_set_invite_limit(global_opts: CommandGlobalOptions, invite_limit: i32) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Get config + + let api_res_get_conf = api_call_get_config(&vault_url, global_opts.debug).await; + + let current_config: VaultConfig = match api_res_get_conf { + Ok(config) => config, + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + }; + + // Changes + + let mut new_config = current_config.clone(); + + new_config.invite_limit = invite_limit; + + // Set config + + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; + + match api_res_set_conf { + Ok(_) => { + eprintln!( + "Successfully changed max number of invited sessions by user: {invite_limit}" + ); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_config_set_preserve_originals( + global_opts: CommandGlobalOptions, + preserve_originals: bool, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Get config + + let api_res_get_conf = api_call_get_config(&vault_url, global_opts.debug).await; + + let current_config: VaultConfig = match api_res_get_conf { + Ok(config) => config, + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + }; + + // Changes + + let mut new_config = current_config.clone(); + + new_config.preserve_originals = preserve_originals; + + // Set config + + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; + + match api_res_set_conf { + Ok(_) => { + eprintln!( + "Successfully changed the option to preserve original files: {preserve_originals}" + ); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + pub async fn run_cmd_config_set_css(global_opts: CommandGlobalOptions, file_path: String) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); @@ -720,8 +918,7 @@ pub async fn run_cmd_config_set_css(global_opts: CommandGlobalOptions, file_path // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -823,8 +1020,7 @@ pub async fn run_cmd_config_clear_css(global_opts: CommandGlobalOptions) { // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -953,8 +1149,7 @@ pub async fn run_cmd_config_add_video_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -1084,8 +1279,7 @@ pub async fn run_cmd_config_remove_video_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -1215,8 +1409,7 @@ pub async fn run_cmd_config_add_image_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { @@ -1348,8 +1541,7 @@ pub async fn run_cmd_config_remove_image_resolution( // Set config - let api_res_set_conf = - api_call_set_config(&vault_url, new_config, global_opts.debug).await; + let api_res_set_conf = api_call_set_config(&vault_url, new_config, global_opts.debug).await; match api_res_set_conf { Ok(_) => { diff --git a/src/models/config.rs b/src/models/config.rs index bf2e61b..9ade03a 100644 --- a/src/models/config.rs +++ b/src/models/config.rs @@ -40,9 +40,15 @@ pub struct VaultConfig { #[serde(rename = "video_previews_interval")] pub video_previews_interval: Option, + #[serde(rename = "invite_limit")] + pub invite_limit: i32, + + #[serde(rename = "preserve_originals", default)] + pub preserve_originals: bool, + #[serde(rename = "resolutions")] pub resolutions: Vec, - + #[serde(rename = "image_resolutions")] pub image_resolutions: Vec, } From 098c5bbbcec060c0f754829e436560aa2b55188e Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sat, 24 May 2025 14:37:51 +0200 Subject: [PATCH 02/12] Add disk usage command --- src/api/about.rs | 20 +++++++++- src/commands/disk_usage.rs | 82 ++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 22 ++++++++-- src/models/about.rs | 16 ++++++++ src/tools/size_render.rs | 20 +++++++--- 5 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 src/commands/disk_usage.rs diff --git a/src/api/about.rs b/src/api/about.rs index c3bc0d7..a681d45 100644 --- a/src/api/about.rs +++ b/src/api/about.rs @@ -1,7 +1,7 @@ // About API use crate::{ - models::ServerInformation, + models::{ServerDiskUsage, ServerInformation}, tools::{do_get_request, RequestError, VaultURI}, }; @@ -22,3 +22,21 @@ pub async fn api_call_about( Ok(parsed_body.unwrap()) } + +pub async fn api_call_disk_usage( + url: &VaultURI, + debug: bool, +) -> Result { + let body_str = do_get_request(url, "/api/about/disk_usage".to_string(), debug).await?; + + let parsed_body: Result = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} diff --git a/src/commands/disk_usage.rs b/src/commands/disk_usage.rs new file mode 100644 index 0000000..e0f3beb --- /dev/null +++ b/src/commands/disk_usage.rs @@ -0,0 +1,82 @@ +// Server info command + +use crate::{ + api::api_call_disk_usage, + commands::logout::do_logout, + tools::{ensure_login, parse_vault_uri, render_size_bytes}, +}; + +use std::process; + +use super::{get_vault_url, print_request_error, CommandGlobalOptions}; + +pub async fn run_cmd_disk_usage(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_disk_usage(&vault_url, global_opts.debug).await; + + match api_res { + Ok(disk_usage) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("---------------------------"); + + println!("Disk usage: {}", disk_usage.usage.round()); + println!("Available: {}", render_size_bytes(disk_usage.available)); + println!("Free: {}", render_size_bytes(disk_usage.free)); + println!("Total size: {}", render_size_bytes(disk_usage.total)); + + println!("---------------------------"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0a76c29..4b0ed66 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -21,6 +21,9 @@ use batch_operation::*; mod config; use config::*; +mod disk_usage; +pub use disk_usage::*; + mod invites; use invites::*; @@ -40,12 +43,12 @@ mod media_export; mod media_extended_description; mod media_image_notes; mod media_import; +mod media_replace; mod media_resolutions; mod media_subtitles; mod media_thumbnail; mod media_time_slices; mod media_upload; -mod media_replace; mod random; use random::*; @@ -277,11 +280,19 @@ pub enum Commands { /// Gets server information, like the version it is using. #[clap(alias("server-info"))] GetServerInformation, + + /// Gets server disk usage. + #[clap(alias("disk-usage"))] + GetDiskUsage, } pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { match cmd { - Commands::Login { username, duration, invite_code } => { + Commands::Login { + username, + duration, + invite_code, + } => { run_cmd_login(global_opts, username, duration, invite_code).await; } Commands::Logout => { @@ -378,10 +389,13 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { } Commands::Invites { invites_cmd } => { run_invites_cmd(global_opts, invites_cmd).await; - }, + } Commands::GetServerInformation => { run_cmd_server_info(global_opts).await; - }, + } + Commands::GetDiskUsage => { + run_cmd_disk_usage(global_opts).await; + } } } diff --git a/src/models/about.rs b/src/models/about.rs index 4c46251..e303610 100644 --- a/src/models/about.rs +++ b/src/models/about.rs @@ -13,3 +13,19 @@ pub struct ServerInformation { #[serde(rename = "ffmpeg_version")] pub ffmpeg_version: String, } + + +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerDiskUsage { + #[serde(rename = "usage")] + pub usage: f32, + + #[serde(rename = "available")] + pub available: u64, + + #[serde(rename = "free")] + pub free: u64, + + #[serde(rename = "total")] + pub total: u64, +} diff --git a/src/tools/size_render.rs b/src/tools/size_render.rs index a01db95..7dd954e 100644 --- a/src/tools/size_render.rs +++ b/src/tools/size_render.rs @@ -1,14 +1,22 @@ // Size rendering +const KB: u64 = 1024; +const MB: u64 = KB * KB; +const GB: u64 = MB * KB; +const TB: u64 = GB * KB; + pub fn render_size_bytes(bytes: u64) -> String { - if bytes > 1024 * 1024 * 1024 { - let v = bytes as f64 / (1024 * 1024 * 1024) as f64; + if bytes > TB { + let v = bytes as f64 / TB as f64; + format!("{v:.2} TB") + } else if bytes > GB { + let v = bytes as f64 / GB as f64; format!("{v:.2} GB") - } else if bytes > 1024 * 1024 { - let v = bytes as f64 / (1024 * 1024) as f64; + } else if bytes > MB { + let v = bytes as f64 / MB as f64; format!("{v:.2} MB") - } else if bytes > 1024 { - let v = bytes as f64 / 1024_f64; + } else if bytes > KB { + let v = bytes as f64 / KB as f64; format!("{v:.2} KB") } else { format!("{bytes} Bytes") From 561788315328235f4bc09e300ce6c217a5543981 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sat, 24 May 2025 14:38:05 +0200 Subject: [PATCH 03/12] Update manual --- MANUAL.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/MANUAL.md b/MANUAL.md index 5958350..85d46ae 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -26,6 +26,7 @@ pmv-cli [OPTIONS] | [invites](#command-invites) | Manages invites | | [batch](#command-batch) | Applies a batch operation to a list of media assets | | [get-server-information](#command-get-server-information) | Gets server information, like the version it is using | +| [get-disk-usage](#command-get-disk-usage) | Gets server disk usage | **Options:** @@ -1467,6 +1468,8 @@ pmv-cli config | [set-max-tasks](#command-config-set-max-tasks) | Sets max tasks in parallel | | [set-encoding-threads](#command-config-set-encoding-threads) | Sets number of encoding threads to use | | [set-video-previews-interval](#command-config-set-video-previews-interval) | Sets the video previews interval in seconds | +| [set-max-invites](#command-config-set-max-invites) | Sets the max number of invited sessions by user | +| [set-preserve-originals](#command-config-set-preserve-originals) | Sets the option to preserve original files, before encoding, as an attachment | | [set-css](#command-config-set-css) | Sets custom CSS for the vault | | [clear-css](#command-config-clear-css) | Clears custom CSS for the vault | | [add-video-resolution](#command-config-add-video-resolution) | Adds video resolution | @@ -1600,6 +1603,50 @@ pmv-cli config set-video-previews-interval | --- | --- | | `-h, --help` | Print help | +### Command: config set-max-invites + +Sets the max number of invited sessions by user + +**Usage:** + +``` +pmv-cli config set-max-invites +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Max number of invited sessions by user | + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: config set-preserve-originals + +Sets the option to preserve original files, before encoding, as an attachment + +**Usage:** + +``` +pmv-cli config set-preserve-originals [PRESERVE_ORIGINALS] +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `[PRESERVE_ORIGINALS]` | Preserve original media, before encoding, as an attachment? | + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + ### Command: config set-css Sets custom CSS for the vault @@ -2094,3 +2141,19 @@ pmv-cli get-server-information | Option | Description | | --- | --- | | `-h, --help` | Print help | + +## Command: get-disk-usage + +Gets server disk usage + +**Usage:** + +``` +pmv-cli get-disk-usage +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | From 4f1b318637da6ec5d01c777d8c298a9ef91cb80a Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sat, 24 May 2025 15:21:47 +0200 Subject: [PATCH 04/12] Bump version (2.0.0) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc9210d..cdbc1b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,7 +954,7 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "pmv-cli" -version = "1.8.0" +version = "2.0.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index d45a5bc..0d6a5a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ description = "Command line interface client for PersonalMediaVault" edition = "2021" license = "MIT" name = "pmv-cli" -version = "1.8.0" +version = "2.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 0d721e7d812fd4740001ef7523d98533b35e6482 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sun, 15 Jun 2025 13:44:33 +0200 Subject: [PATCH 05/12] Update login to support two factor authentication --- build-production.bat | 2 ++ src/commands/login.rs | 3 +- src/commands/mod.rs | 7 +++- src/models/auth.rs | 3 ++ src/tools/ensure_login.rs | 72 +++++++++++++++++++++++++++++++++------ src/tools/request.rs | 2 +- 6 files changed, 76 insertions(+), 13 deletions(-) diff --git a/build-production.bat b/build-production.bat index 60bf17d..7e98106 100644 --- a/build-production.bat +++ b/build-production.bat @@ -1,3 +1,5 @@ @echo off call cargo build --release + +call cp -f target/release/pmv-cli.exe pmv-cli.exe diff --git a/src/commands/login.rs b/src/commands/login.rs index 66d8d80..b51cfc6 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -18,6 +18,7 @@ pub async fn run_cmd_login( username: Option, duration: Option, invite_code: Option, + tfa_code: Option, ) { let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); @@ -134,7 +135,7 @@ pub async fn run_cmd_login( } None => { let login_result = - ensure_login_ext(&vault_url, &username, &duration, global_opts.debug).await; + ensure_login_ext(&vault_url, &username, &None, &tfa_code, &duration, global_opts.debug, false).await; if login_result.is_err() { process::exit(1); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4b0ed66..39e3873 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -94,6 +94,10 @@ pub enum Commands { /// Invite code. Setting this option will ignore the credentials and use the code. #[arg(short = 'I', long)] invite_code: Option, + + /// Two factor authentication code + #[arg(short = 'T', long)] + tfa_code: Option, }, /// Closes the active session, given a session URL @@ -292,8 +296,9 @@ pub async fn run_cmd(global_opts: CommandGlobalOptions, cmd: Commands) { username, duration, invite_code, + tfa_code, } => { - run_cmd_login(global_opts, username, duration, invite_code).await; + run_cmd_login(global_opts, username, duration, invite_code, tfa_code).await; } Commands::Logout => { run_cmd_logout(global_opts).await; diff --git a/src/models/auth.rs b/src/models/auth.rs index df9b8b3..b94c95f 100644 --- a/src/models/auth.rs +++ b/src/models/auth.rs @@ -12,6 +12,9 @@ pub struct Credentials { #[serde(rename = "duration")] pub duration: Option, + + #[serde(rename = "tfaCode")] + pub tfa_code: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/tools/ensure_login.rs b/src/tools/ensure_login.rs index 91b0740..1f2f31b 100644 --- a/src/tools/ensure_login.rs +++ b/src/tools/ensure_login.rs @@ -12,8 +12,11 @@ use super::VaultURI; pub async fn ensure_login_ext( url: &VaultURI, given_username: &Option, + given_password: &Option, + given_tfa_code: &Option, duration: &Option, debug: bool, + required_tfa: bool, ) -> Result { match url.clone() { VaultURI::LoginURI { @@ -34,14 +37,35 @@ pub async fn ensure_login_ext( } }; + let tfa_code_m = match given_tfa_code { + Some(code) => Some(code.clone()), + None => { + if required_tfa { + let code = ask_user("Two factor authentication code: ") + .await + .unwrap_or("".to_string()); + Some(code) + } else { + None + } + } + }; + let mut password_m = password.clone(); if password_m.is_empty() { - // Ask password - eprintln!("Input password for vault: {base_url}"); - password_m = ask_user_password("Password: ") - .await - .unwrap_or("".to_string()); + match given_password { + Some(p) => { + password_m = p.clone(); + } + None => { + // Ask password + eprintln!("Input password for vault: {base_url}"); + password_m = ask_user_password("Password: ") + .await + .unwrap_or("".to_string()); + } + } } // Login @@ -49,17 +73,45 @@ pub async fn ensure_login_ext( let login_res = api_call_login( url, Credentials { - username: username_m, - password: password_m, + username: username_m.clone(), + password: password_m.clone(), duration: duration.clone(), + tfa_code: tfa_code_m, }, debug, ) .await; if login_res.is_err() { - print_request_error(login_res.err().unwrap()); - return Err(()); + let error = login_res.err().unwrap(); + + match error.clone() { + super::RequestError::Api { + status, + code, + message: _, + } => { + if status == 403 && code == "TFA_REQUIRED" { + return Box::pin(ensure_login_ext( + url, + &Some(username_m), + &Some(password_m), + &None, + duration, + debug, + true, + )) + .await; + } else { + print_request_error(error); + return Err(()); + } + } + _ => { + print_request_error(error); + return Err(()); + } + } } let session_id = login_res.unwrap().session_id; @@ -87,5 +139,5 @@ pub async fn ensure_login( given_username: &Option, debug: bool, ) -> Result { - ensure_login_ext(url, given_username, &None, debug).await + ensure_login_ext(url, given_username, &None, &None, &None, debug, false).await } diff --git a/src/tools/request.rs b/src/tools/request.rs index e9a7e44..a9fa0ac 100644 --- a/src/tools/request.rs +++ b/src/tools/request.rs @@ -6,7 +6,7 @@ use super::vault_uri::VaultURI; pub const SESSION_HEADER_NAME: &str = "x-session-token"; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RequestError { StatusCode(reqwest::StatusCode), Api { From 4e2a09ebec743051a149ecadc87827a5017085f0 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sun, 15 Jun 2025 14:13:51 +0200 Subject: [PATCH 06/12] Support auth confirmation --- src/api/media.rs | 5 +- src/tools/request.rs | 109 +++++++++++++++++++++++++++++++----- src/tools/request_upload.rs | 60 ++++++++++++++++++++ 3 files changed, 156 insertions(+), 18 deletions(-) diff --git a/src/api/media.rs b/src/api/media.rs index d476729..cf5609f 100644 --- a/src/api/media.rs +++ b/src/api/media.rs @@ -7,8 +7,7 @@ use crate::{ ImageNote, MediaAssetSizeStats, MediaAttachment, MediaAudioTrack, MediaMetadata, MediaRenameAttachmentBody, MediaRenameSubtitleOrAudioBody, MediaResolution, MediaSubtitle, MediaTimeSlice, MediaUpdateDescriptionBody, MediaUpdateExtendedDescriptionBody, MediaUpdateExtraBody, MediaUpdateThumbnailResponse, MediaUpdateTitleBody, MediaUploadResponse, TaskEncodeResolution }, tools::{ - do_get_request, do_multipart_upload_request, do_post_request, ProgressReceiver, - RequestError, VaultURI, + do_get_request, do_multipart_upload_request, do_multipart_upload_request_with_confirmation, do_post_request, ProgressReceiver, RequestError, VaultURI }, }; @@ -270,7 +269,7 @@ pub async fn api_call_media_replace( debug: bool, progress_receiver: Arc>, ) -> Result<(), RequestError> { - do_multipart_upload_request( + do_multipart_upload_request_with_confirmation( url, format!("/api/media/{media}/replace"), "file".to_string(), diff --git a/src/tools/request.rs b/src/tools/request.rs index a9fa0ac..44798dd 100644 --- a/src/tools/request.rs +++ b/src/tools/request.rs @@ -1,10 +1,14 @@ // HTTP requests +use crate::tools::{ask_user, ask_user_password}; + use super::super::models::*; use super::vault_uri::VaultURI; pub const SESSION_HEADER_NAME: &str = "x-session-token"; +pub const AUTH_CONFIRMATION_PASSWORD_HEADER_NAME: &str = "x-auth-confirmation-pw"; +pub const AUTH_CONFIRMATION_TFA_HEADER_NAME: &str = "x-auth-confirmation-tfa"; #[derive(Debug, Clone)] pub enum RequestError { @@ -50,7 +54,9 @@ pub fn get_session_from_uri(uri: VaultURI) -> Option { } } -pub async fn send_request(request_builder: reqwest::RequestBuilder) -> Result { +pub async fn send_request( + request_builder: reqwest::RequestBuilder, +) -> Result { let response_result = request_builder.send().await; match response_result { @@ -65,8 +71,9 @@ pub async fn send_request(request_builder: reqwest::RequestBuilder) -> Result { if res_status != reqwest::StatusCode::OK { if !res_body.is_empty() { - let parsed_body: Result = serde_json::from_str(&res_body); - + let parsed_body: Result = + serde_json::from_str(&res_body); + match parsed_body { Ok(r) => { return Err(RequestError::Api { @@ -80,20 +87,16 @@ pub async fn send_request(request_builder: reqwest::RequestBuilder) -> Result { - Err(RequestError::NetworkError(err.to_string())) - }, + } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } - }, - Err(err) => { - Err(RequestError::NetworkError(err.to_string())) - }, + } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } } @@ -125,11 +128,79 @@ pub async fn do_get_request( send_request(request_builder).await } +pub async fn request_auth_confirmation_password() -> String { + eprintln!("Input your account password to confirm the operation"); + ask_user_password("Password: ") + .await + .unwrap_or("".to_string()) +} + +pub async fn request_auth_confirmation_tfa() -> String { + eprintln!("Input your one-time two factor authentication code to confirm the operation"); + ask_user("Two factor authentication code: ") + .await + .unwrap_or("".to_string()) +} + pub async fn do_post_request( uri: &VaultURI, path: String, body: String, debug: bool, +) -> Result { + let final_uri = resolve_vault_api_uri(uri.clone(), path.clone()); + + if debug { + eprintln!("\rDEBUG: POST {final_uri}"); + } + + let client = reqwest::Client::new(); + + let mut request_builder = client + .post(final_uri) + .header("Content-Type", "application/json") + .body(body.clone()); + + let session = get_session_from_uri(uri.clone()); + + if let Some(s) = session { + request_builder = request_builder.header(SESSION_HEADER_NAME, s); + } + + match send_request(request_builder).await { + Ok(r) => Ok(r), + Err(err) => match err.clone() { + RequestError::Api { + status, + code, + message: _, + } => { + if status == 403 { + if code == "AUTH_CONFIRMATION_REQUIRED_TFA" { + let confirmation_tfa = request_auth_confirmation_tfa().await; + do_post_request_with_confirmation(uri, path, body, debug, None, Some(confirmation_tfa)).await + } else if code == "AUTH_CONFIRMATION_REQUIRED_PW" { + let confirmation_pw = request_auth_confirmation_password().await; + do_post_request_with_confirmation(uri, path, body, debug, Some(confirmation_pw), None).await + } else { + Err(err) + } + } else { + Err(err) + } + } + _ => Err(err), + }, + } +} + +pub async fn do_post_request_with_confirmation( + uri: &VaultURI, + path: String, + body: String, + debug: bool, + confirmation_password: Option, + confirmation_tfa: Option, ) -> Result { let final_uri = resolve_vault_api_uri(uri.clone(), path); @@ -139,7 +210,8 @@ pub async fn do_post_request( let client = reqwest::Client::new(); - let mut request_builder = client.post(final_uri) + let mut request_builder = client + .post(final_uri) .header("Content-Type", "application/json") .body(body); @@ -149,10 +221,17 @@ pub async fn do_post_request( request_builder = request_builder.header(SESSION_HEADER_NAME, s); } + if let Some(cp) = confirmation_password { + request_builder = request_builder.header(AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, cp); + } + + if let Some(ct) = confirmation_tfa { + request_builder = request_builder.header(AUTH_CONFIRMATION_TFA_HEADER_NAME, ct); + } + send_request(request_builder).await } - pub async fn do_delete_request( uri: &VaultURI, path: String, diff --git a/src/tools/request_upload.rs b/src/tools/request_upload.rs index 9494f64..ce5547f 100644 --- a/src/tools/request_upload.rs +++ b/src/tools/request_upload.rs @@ -6,6 +6,8 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use tokio_sync_read_stream::SyncReadStream; +use crate::tools::{request_auth_confirmation_password, request_auth_confirmation_tfa, AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, AUTH_CONFIRMATION_TFA_HEADER_NAME}; + use super::{super::models::*, ProgressReceiver}; use super::{get_session_from_uri, resolve_vault_api_uri, RequestError, SESSION_HEADER_NAME}; @@ -110,6 +112,43 @@ impl std::io::Read for UploadProgressReporterSync { } } +pub async fn do_multipart_upload_request_with_confirmation( + uri: &VaultURI, + path: String, + field: String, + file_path: String, + debug: bool, + progress_receiver: Arc>, +) -> Result { + let res = do_multipart_upload_request_internal(uri, path.clone(), field.clone(), file_path.clone(), debug, None, None, progress_receiver.clone()).await; + + match res { + Ok(r) => Ok(r), + Err(err) => { + match err.clone() { + RequestError::Api { status, code, message: _ } => { + if status == 403 { + if code == "AUTH_CONFIRMATION_REQUIRED_TFA" { + let confirmation_tfa = request_auth_confirmation_tfa().await; + do_multipart_upload_request_internal(uri, path.clone(), field.clone(), file_path.clone(), debug, None, Some(confirmation_tfa), progress_receiver.clone()).await + } else if code == "AUTH_CONFIRMATION_REQUIRED_PW" { + let confirmation_pw = request_auth_confirmation_password().await; + do_multipart_upload_request_internal(uri, path.clone(), field.clone(), file_path.clone(), debug, Some(confirmation_pw), None, progress_receiver.clone()).await + } else { + Err(err) + } + } else { + Err(err) + } + }, + _ => { + Err(err) + } + } + }, + } +} + pub async fn do_multipart_upload_request( uri: &VaultURI, path: String, @@ -117,6 +156,19 @@ pub async fn do_multipart_upload_request( file_path: String, debug: bool, progress_receiver: Arc>, +) -> Result { + do_multipart_upload_request_internal(uri, path, field, file_path, debug, None, None, progress_receiver).await +} + +pub async fn do_multipart_upload_request_internal( + uri: &VaultURI, + path: String, + field: String, + file_path: String, + debug: bool, + confirmation_password: Option, + confirmation_tfa: Option, + progress_receiver: Arc>, ) -> Result { let final_uri = resolve_vault_api_uri(uri.clone(), path); @@ -178,6 +230,14 @@ pub async fn do_multipart_upload_request( request_builder = request_builder.header(SESSION_HEADER_NAME, s); } + if let Some(cp) = confirmation_password { + request_builder = request_builder.header(AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, cp); + } + + if let Some(ct) = confirmation_tfa { + request_builder = request_builder.header(AUTH_CONFIRMATION_TFA_HEADER_NAME, ct); + } + // Send request let response_result = request_builder.send().await; From 0418a88bdbf43c6ee8b589f6a589040a6f5e5958 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Tue, 17 Jun 2025 17:17:44 +0200 Subject: [PATCH 07/12] Add new models and APIs --- src/api/account.rs | 108 +++++++++++++++++++++++++++++++++- src/models/account.rs | 134 ++++++++++++++++++++++++++++++++++++++++++ ws.code-workspace | 3 + 3 files changed, 244 insertions(+), 1 deletion(-) diff --git a/src/api/account.rs b/src/api/account.rs index e7c1d12..f8a443f 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -2,7 +2,7 @@ use crate::{ models::{ - AccountContext, AccountCreateBody, AccountDeleteBody, AccountListItem, AccountUpdateBody, ChangePasswordBody, ChangeUsernameBody + AccountContext, AccountCreateBody, AccountDeleteBody, AccountListItem, AccountSecuritySettings, AccountSetSecuritySettingsBody, AccountUpdateBody, ChangePasswordBody, ChangeUsernameBody, TfaDisableBody, TimeOtpEnableBody, TimeOtpOptions, TimeOtpSettings }, tools::{do_get_request, do_post_request, RequestError, VaultURI}, }; @@ -119,3 +119,109 @@ pub async fn api_call_delete_account( Ok(()) } + + +pub async fn api_call_get_account_settings( + url: &VaultURI, + debug: bool, +) -> Result { + let body_str = do_get_request(url, "/api/account/security".to_string(), debug).await?; + + let parsed_body: Result = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + +pub async fn api_call_set_account_settings( + url: &VaultURI, + req_body: AccountSetSecuritySettingsBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + "/api/account/security".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_get_totp_settings( + url: &VaultURI, + options: TimeOtpOptions, + debug: bool, +) -> Result { + let mut url_path = format!("/api/account/security/tfa/totp"); + + url_path.push_str(&("?algorithm=".to_owned() + &urlencoding::encode(&options.algorithm.to_string()))); + url_path.push_str(&("&period=".to_owned() + &urlencoding::encode(&options.period.to_string()))); + + if let Some(issuer) = options.issuer { + url_path.push_str(&("&issuer=".to_owned() + &urlencoding::encode(&issuer))); + } + + if let Some(account) = options.account { + url_path.push_str(&("&account=".to_owned() + &urlencoding::encode(&account))); + } + + if options.skew { + url_path.push_str(&"&skew=allow".to_owned()); + } else { + url_path.push_str(&"&skew=disallow".to_owned()); + } + + let body_str = do_get_request(url, "/api/account/security".to_string(), debug).await?; + + let parsed_body: Result = serde_json::from_str(&body_str); + + if parsed_body.is_err() { + return Err(RequestError::Json { + message: parsed_body.err().unwrap().to_string(), + body: body_str, + }); + } + + Ok(parsed_body.unwrap()) +} + + +pub async fn api_call_enable_totp( + url: &VaultURI, + req_body: TimeOtpEnableBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + "/api/account/security/tfa/totp".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} + +pub async fn api_call_disable_tfa( + url: &VaultURI, + req_body: TfaDisableBody, + debug: bool, +) -> Result<(), RequestError> { + do_post_request( + url, + "/api/account/security/tfa/disable".to_string(), + serde_json::to_string(&req_body).unwrap(), + debug, + ) + .await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/models/account.rs b/src/models/account.rs index fcc34b3..9fdb059 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -78,3 +78,137 @@ pub struct AccountDeleteBody { #[serde(rename = "username")] pub username: String, } + + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountSecuritySettings { + #[serde(rename = "tfa")] + pub tfa: bool, + + #[serde(rename = "tfaMethod")] + pub tfa_method: String, + + #[serde(rename = "authConfirmation")] + pub auth_confirmation: bool, + + #[serde(rename = "authConfirmationMethod")] + pub auth_confirmation_method: String, + + #[serde(rename = "authConfirmationPeriodSeconds")] + pub auth_confirmation_period_seconds: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AccountSetSecuritySettingsBody { + #[serde(rename = "authConfirmation")] + pub auth_confirmation: bool, + + #[serde(rename = "authConfirmationMethod")] + pub auth_confirmation_method: String, + + #[serde(rename = "authConfirmationPeriodSeconds")] + pub auth_confirmation_period_seconds: i32, +} + + +#[derive(Debug, Copy, Clone)] +pub enum TimeOtpAlgorithm { + Sha1, + Sha256, + Sha512, +} + +impl TimeOtpAlgorithm { + pub fn parse(input: &str) -> Result { + match input.to_lowercase().as_str() { + "sha1" | "sha-1" => Ok(TimeOtpAlgorithm::Sha1), + "sha256" | "sha-256" => Ok(TimeOtpAlgorithm::Sha256), + "sha512" | "sha-512" => Ok(TimeOtpAlgorithm::Sha512), + _ => Err(()), + } + } +} + +impl ToString for TimeOtpAlgorithm { + fn to_string(&self) -> String { + match self { + TimeOtpAlgorithm::Sha1 => "sha1".to_string(), + TimeOtpAlgorithm::Sha256 => "sha256".to_string(), + TimeOtpAlgorithm::Sha512 => "sha512".to_string(), + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum TimeOtpPeriod { + P30, + P60, + P120, +} + +impl TimeOtpPeriod { + pub fn parse(input: &str) -> Result { + match input.to_lowercase().as_str() { + "30s" | "30" => Ok(TimeOtpPeriod::P30), + "1m" | "60s" | "60" => Ok(TimeOtpPeriod::P60), + "2m" | "120s" | "120" => Ok(TimeOtpPeriod::P120), + _ => Err(()), + } + } +} + +impl ToString for TimeOtpPeriod { + fn to_string(&self) -> String { + match self { + TimeOtpPeriod::P30 => "30".to_string(), + TimeOtpPeriod::P60 => "60".to_string(), + TimeOtpPeriod::P120 => "120".to_string(), + } + } +} + +#[derive(Debug)] +pub struct TimeOtpOptions { + pub issuer: Option, + + pub account: Option, + + pub algorithm: TimeOtpAlgorithm, + + pub period: TimeOtpPeriod, + + pub skew: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TimeOtpSettings { + #[serde(rename = "secret")] + pub secret: String, + + #[serde(rename = "method")] + pub method: String, + + #[serde(rename = "url")] + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TimeOtpEnableBody { + #[serde(rename = "secret")] + pub secret: String, + + #[serde(rename = "method")] + pub method: String, + + #[serde(rename = "password")] + pub password: String, + + #[serde(rename = "code")] + pub code: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TfaDisableBody { + #[serde(rename = "code")] + pub code: String, +} diff --git a/ws.code-workspace b/ws.code-workspace index 0576264..7d1f4c2 100644 --- a/ws.code-workspace +++ b/ws.code-workspace @@ -13,5 +13,8 @@ ".\\Cargo.toml" ], "rust-analyzer.showUnlinkedFileNotification": false, + "cSpell.words": [ + "totp" + ], } } \ No newline at end of file From 36c5d4c66ed19123e06c3f271f36f17250f3e100 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Wed, 18 Jun 2025 17:31:41 +0200 Subject: [PATCH 08/12] Add commands to get or set security settings --- src/api/account.rs | 4 +- src/commands/account.rs | 204 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/src/api/account.rs b/src/api/account.rs index f8a443f..0ef3d54 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -121,7 +121,7 @@ pub async fn api_call_delete_account( } -pub async fn api_call_get_account_settings( +pub async fn api_call_get_security_settings( url: &VaultURI, debug: bool, ) -> Result { @@ -139,7 +139,7 @@ pub async fn api_call_get_account_settings( Ok(parsed_body.unwrap()) } -pub async fn api_call_set_account_settings( +pub async fn api_call_set_security_settings( url: &VaultURI, req_body: AccountSetSecuritySettingsBody, debug: bool, diff --git a/src/commands/account.rs b/src/commands/account.rs index 3152635..75bc612 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -7,12 +7,11 @@ use clap::Subcommand; use crate::{ api::{ api_call_change_password, api_call_change_username, api_call_context, - api_call_create_account, api_call_delete_account, api_call_list_accounts, - api_call_update_account, + api_call_create_account, api_call_delete_account, api_call_get_security_settings, + api_call_list_accounts, api_call_set_security_settings, api_call_update_account, }, models::{ - AccountCreateBody, AccountDeleteBody, AccountUpdateBody, ChangePasswordBody, - ChangeUsernameBody, + AccountCreateBody, AccountDeleteBody, AccountSetSecuritySettingsBody, AccountUpdateBody, ChangePasswordBody, ChangeUsernameBody }, tools::{ ask_user, ask_user_password, ensure_login, parse_vault_uri, print_table, to_csv_string, @@ -35,6 +34,22 @@ pub enum AccountCommand { /// Changes account password ChangePassword, + /// Gets account security settings + GetSecuritySettings, + + SetAuthConfirmation { + /// Set to 'true' to enable auth confirmation, Set it to 'false' to disable it + auth_confirmation: bool, + + /// Prefer using the account password instead of two factor authentication + #[arg(long)] + prefer_password: bool, + + /// Period (seconds) to remember the last auth confirmation + #[arg(long)] + period_seconds: i32, + }, + /// List accounts #[clap(alias("ls"))] List { @@ -104,6 +119,22 @@ pub async fn run_account_cmd(global_opts: CommandGlobalOptions, cmd: AccountComm AccountCommand::Delete { username } => { run_cmd_delete_account(global_opts, username).await; } + AccountCommand::GetSecuritySettings => { + run_cmd_get_account_security(global_opts).await; + } + AccountCommand::SetAuthConfirmation { + auth_confirmation, + prefer_password, + period_seconds, + } => { + run_cmd_set_account_security( + global_opts, + auth_confirmation, + prefer_password, + period_seconds, + ) + .await; + } } } @@ -777,3 +808,168 @@ pub async fn run_cmd_delete_account(global_opts: CommandGlobalOptions, username: } } } + +pub async fn run_cmd_get_account_security(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_get_security_settings(&vault_url, global_opts.debug).await; + + match api_res { + Ok(res) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("---------------------------"); + + if res.auth_confirmation { + println!("Auth confirmation: Enabled"); + + if res.auth_confirmation_method == "pw" { + println!("Auth confirmation preferred method: Account password"); + } else { + println!("Auth confirmation preferred method: Two factor authentication"); + } + + println!( + "Auth confirmation period: {} seconds", + res.auth_confirmation_period_seconds + ); + } else { + println!("Auth confirmation: Disabled"); + } + + println!("---------------------------"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_set_account_security( + global_opts: CommandGlobalOptions, + auth_confirmation: bool, + prefer_password: bool, + period_seconds: i32, +) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_set_security_settings( + &vault_url, + AccountSetSecuritySettingsBody { + auth_confirmation, + auth_confirmation_method: if prefer_password { + "pw".to_string() + } else { + "tfa".to_string() + }, + auth_confirmation_period_seconds: period_seconds, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + eprintln!("Successfully updated account security settings"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} From b1a7756934c1b046107b1aa0f2531f5703360081 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Thu, 19 Jun 2025 18:52:01 +0200 Subject: [PATCH 09/12] Implement rest of v2 commands --- src/api/account.rs | 6 +- src/commands/account.rs | 349 ++++++++++++++++++++++++++++++++++-- src/models/account.rs | 218 +++++++++++----------- src/tools/request_upload.rs | 116 ++++++++---- 4 files changed, 536 insertions(+), 153 deletions(-) diff --git a/src/api/account.rs b/src/api/account.rs index 0ef3d54..20dd8b0 100644 --- a/src/api/account.rs +++ b/src/api/account.rs @@ -160,7 +160,7 @@ pub async fn api_call_get_totp_settings( options: TimeOtpOptions, debug: bool, ) -> Result { - let mut url_path = format!("/api/account/security/tfa/totp"); + let mut url_path = "/api/account/security/tfa/totp".to_string(); url_path.push_str(&("?algorithm=".to_owned() + &urlencoding::encode(&options.algorithm.to_string()))); url_path.push_str(&("&period=".to_owned() + &urlencoding::encode(&options.period.to_string()))); @@ -174,9 +174,9 @@ pub async fn api_call_get_totp_settings( } if options.skew { - url_path.push_str(&"&skew=allow".to_owned()); + url_path.push_str("&skew=allow"); } else { - url_path.push_str(&"&skew=disallow".to_owned()); + url_path.push_str("&skew=disallow"); } let body_str = do_get_request(url, "/api/account/security".to_string(), debug).await?; diff --git a/src/commands/account.rs b/src/commands/account.rs index 75bc612..dc1954a 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -7,14 +7,18 @@ use clap::Subcommand; use crate::{ api::{ api_call_change_password, api_call_change_username, api_call_context, - api_call_create_account, api_call_delete_account, api_call_get_security_settings, + api_call_create_account, api_call_delete_account, api_call_disable_tfa, + api_call_enable_totp, api_call_get_security_settings, api_call_get_totp_settings, api_call_list_accounts, api_call_set_security_settings, api_call_update_account, }, models::{ - AccountCreateBody, AccountDeleteBody, AccountSetSecuritySettingsBody, AccountUpdateBody, ChangePasswordBody, ChangeUsernameBody + AccountCreateBody, AccountDeleteBody, AccountSetSecuritySettingsBody, AccountUpdateBody, + ChangePasswordBody, ChangeUsernameBody, TfaDisableBody, TimeOtpAlgorithm, + TimeOtpEnableBody, TimeOtpOptions, TimeOtpPeriod, }, tools::{ - ask_user, ask_user_password, ensure_login, parse_vault_uri, print_table, to_csv_string, + ask_user, ask_user_password, ensure_login, parse_vault_uri, print_table, + request_auth_confirmation_password, request_auth_confirmation_tfa, to_csv_string, }, }; @@ -37,9 +41,10 @@ pub enum AccountCommand { /// Gets account security settings GetSecuritySettings, + /// Sets auth confirmation options SetAuthConfirmation { /// Set to 'true' to enable auth confirmation, Set it to 'false' to disable it - auth_confirmation: bool, + auth_confirmation: String, /// Prefer using the account password instead of two factor authentication #[arg(long)] @@ -47,10 +52,45 @@ pub enum AccountCommand { /// Period (seconds) to remember the last auth confirmation #[arg(long)] - period_seconds: i32, + period_seconds: Option, }, - /// List accounts + /// Gets TOTP settings in order to enable two factor authentication + GetTotpSettings { + /// TOTP issuer (to be added th the URL) + #[arg(long)] + issuer: Option, + + /// TOTP account (to be added th the URL) + #[arg(long)] + account: Option, + + /// Hashing algorithm (sha-1, sha-256 or sha-512) + #[arg(long)] + algorithm: Option, + + /// TOTP period (30s, 60s or 120s) + #[arg(long)] + period: Option, + + /// Allows clock skew of 1 period + #[arg(long)] + allow_skew: bool, + }, + + /// Enables two factor authentication + EnableTfa { + /// Two factor authentication method (from the settings command result) + method: String, + + /// Two factor authentication secret + secret: String, + }, + + /// Disables two factor authentication + DisableTfa, + + /// Lists accounts #[clap(alias("ls"))] List { /// CSV format @@ -135,6 +175,22 @@ pub async fn run_account_cmd(global_opts: CommandGlobalOptions, cmd: AccountComm ) .await; } + AccountCommand::GetTotpSettings { + issuer, + account, + algorithm, + period, + allow_skew, + } => { + run_cmd_get_totp_settings(global_opts, issuer, account, algorithm, period, allow_skew) + .await; + } + AccountCommand::EnableTfa { method, secret } => { + run_cmd_enable_tfa(global_opts, method, secret).await; + } + AccountCommand::DisableTfa => { + run_cmd_disable_tfa(global_opts).await; + } } } @@ -894,10 +950,19 @@ pub async fn run_cmd_get_account_security(global_opts: CommandGlobalOptions) { pub async fn run_cmd_set_account_security( global_opts: CommandGlobalOptions, - auth_confirmation: bool, + auth_confirmation: String, prefer_password: bool, - period_seconds: i32, + period_seconds: Option, ) { + let auth_confirmation_bool = match auth_confirmation.to_lowercase().as_str() { + "0" | "false" | "no" => false, + "1" | "true" | "yes" => true, + _ => { + eprintln!("Invalid argument: Set it to TRUE or FALSE"); + process::exit(1); + } + }; + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); if url_parse_res.is_err() { @@ -930,13 +995,13 @@ pub async fn run_cmd_set_account_security( let api_res = api_call_set_security_settings( &vault_url, AccountSetSecuritySettingsBody { - auth_confirmation, + auth_confirmation: auth_confirmation_bool, auth_confirmation_method: if prefer_password { "pw".to_string() } else { "tfa".to_string() }, - auth_confirmation_period_seconds: period_seconds, + auth_confirmation_period_seconds: period_seconds.unwrap_or(120), }, global_opts.debug, ) @@ -973,3 +1038,267 @@ pub async fn run_cmd_set_account_security( } } } + +pub async fn run_cmd_get_totp_settings( + global_opts: CommandGlobalOptions, + issuer: Option, + account: Option, + algorithm: Option, + period: Option, + allow_skew: bool, +) { + let hash_algorithm = match algorithm { + Some(a) => match TimeOtpAlgorithm::parse(&a) { + Ok(p) => p, + Err(_) => { + eprintln!("Invalid hash algorithm. Valid ones are: sha-1, sha-256, sha-512"); + process::exit(1); + } + }, + None => TimeOtpAlgorithm::Sha1, + }; + + let time_period = match period { + Some(a) => match TimeOtpPeriod::parse(&a) { + Ok(p) => p, + Err(_) => { + eprintln!("Invalid period. Valid ones are: 30s, 60s, 120s"); + process::exit(1); + } + }, + None => TimeOtpPeriod::P30, + }; + + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Call API + + let api_res = api_call_get_totp_settings( + &vault_url, + TimeOtpOptions { + issuer, + account, + algorithm: hash_algorithm, + period: time_period, + skew: allow_skew, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(res) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + println!("---------------------------"); + + println!("Method: {}", res.method); + println!("Secret: {}", res.secret); + println!("URL: {}", res.url); + + println!("---------------------------"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_enable_tfa(global_opts: CommandGlobalOptions, method: String, secret: String) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Ask for confirmation + + let confirmation_pw = request_auth_confirmation_password().await; + let confirmation_tfa = request_auth_confirmation_tfa().await; + + // Call API + + let api_res = api_call_enable_totp( + &vault_url, + TimeOtpEnableBody { + method, + secret, + password: confirmation_pw, + code: confirmation_tfa, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + eprintln!("Successfully enabled two factor authentication"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} + +pub async fn run_cmd_disable_tfa(global_opts: CommandGlobalOptions) { + let url_parse_res = parse_vault_uri(get_vault_url(&global_opts.vault_url)); + + if url_parse_res.is_err() { + match url_parse_res.err().unwrap() { + crate::tools::VaultURIParseError::InvalidProtocol => { + eprintln!("Invalid vault URL provided. Must be an HTTP or HTTPS URL."); + } + crate::tools::VaultURIParseError::URLError(e) => { + let err_msg = e.to_string(); + eprintln!("Invalid vault URL provided: {err_msg}"); + } + } + + process::exit(1); + } + + let mut vault_url = url_parse_res.unwrap(); + + let logout_after_operation = vault_url.is_login(); + let login_result = ensure_login(&vault_url, &None, global_opts.debug).await; + + if login_result.is_err() { + process::exit(1); + } + + vault_url = login_result.unwrap(); + + // Ask for confirmation + + let confirmation_tfa = request_auth_confirmation_tfa().await; + + // Call API + + let api_res = api_call_disable_tfa( + &vault_url, + TfaDisableBody { + code: confirmation_tfa, + }, + global_opts.debug, + ) + .await; + + match api_res { + Ok(_) => { + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + + eprintln!("Successfully disabled two factor authentication"); + } + Err(e) => { + print_request_error(e); + if logout_after_operation { + let logout_res = do_logout(&global_opts, &vault_url).await; + + match logout_res { + Ok(_) => {} + Err(_) => { + process::exit(1); + } + } + } + process::exit(1); + } + } +} diff --git a/src/models/account.rs b/src/models/account.rs index 9fdb059..05fd42f 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -1,214 +1,216 @@ // Models for account API +use std::fmt::Display; + use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct AccountContext { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "title")] - pub title: Option, + #[serde(rename = "title")] + pub title: Option, - #[serde(rename = "css")] - pub css: Option, + #[serde(rename = "css")] + pub css: Option, - #[serde(rename = "root")] - pub root: bool, + #[serde(rename = "root")] + pub root: bool, - #[serde(rename = "write")] - pub write: bool, + #[serde(rename = "write")] + pub write: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct ChangeUsernameBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, } #[derive(Debug, Serialize, Deserialize)] pub struct ChangePasswordBody { - #[serde(rename = "old_password")] - pub old_password: String, + #[serde(rename = "old_password")] + pub old_password: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, } #[derive(Debug, Serialize, Deserialize)] pub struct AccountListItem { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "write")] - pub write: bool, + #[serde(rename = "write")] + pub write: bool, } - #[derive(Debug, Serialize, Deserialize)] pub struct AccountCreateBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, - #[serde(rename = "write")] - pub write: bool, + #[serde(rename = "write")] + pub write: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct AccountUpdateBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, - #[serde(rename = "newUsername")] - pub new_username: Option, + #[serde(rename = "newUsername")] + pub new_username: Option, - #[serde(rename = "write")] - pub write: Option, + #[serde(rename = "write")] + pub write: Option, } - #[derive(Debug, Serialize, Deserialize)] pub struct AccountDeleteBody { - #[serde(rename = "username")] - pub username: String, + #[serde(rename = "username")] + pub username: String, } - #[derive(Debug, Serialize, Deserialize)] pub struct AccountSecuritySettings { - #[serde(rename = "tfa")] - pub tfa: bool, + #[serde(rename = "tfa")] + pub tfa: bool, - #[serde(rename = "tfaMethod")] - pub tfa_method: String, + #[serde(rename = "tfaMethod")] + pub tfa_method: String, - #[serde(rename = "authConfirmation")] - pub auth_confirmation: bool, + #[serde(rename = "authConfirmation")] + pub auth_confirmation: bool, - #[serde(rename = "authConfirmationMethod")] - pub auth_confirmation_method: String, + #[serde(rename = "authConfirmationMethod")] + pub auth_confirmation_method: String, - #[serde(rename = "authConfirmationPeriodSeconds")] - pub auth_confirmation_period_seconds: i32, + #[serde(rename = "authConfirmationPeriodSeconds")] + pub auth_confirmation_period_seconds: i32, } #[derive(Debug, Serialize, Deserialize)] pub struct AccountSetSecuritySettingsBody { - #[serde(rename = "authConfirmation")] - pub auth_confirmation: bool, + #[serde(rename = "authConfirmation")] + pub auth_confirmation: bool, - #[serde(rename = "authConfirmationMethod")] - pub auth_confirmation_method: String, + #[serde(rename = "authConfirmationMethod")] + pub auth_confirmation_method: String, - #[serde(rename = "authConfirmationPeriodSeconds")] - pub auth_confirmation_period_seconds: i32, + #[serde(rename = "authConfirmationPeriodSeconds")] + pub auth_confirmation_period_seconds: i32, } - #[derive(Debug, Copy, Clone)] pub enum TimeOtpAlgorithm { - Sha1, - Sha256, - Sha512, + Sha1, + Sha256, + Sha512, } impl TimeOtpAlgorithm { - pub fn parse(input: &str) -> Result { - match input.to_lowercase().as_str() { - "sha1" | "sha-1" => Ok(TimeOtpAlgorithm::Sha1), - "sha256" | "sha-256" => Ok(TimeOtpAlgorithm::Sha256), - "sha512" | "sha-512" => Ok(TimeOtpAlgorithm::Sha512), - _ => Err(()), + pub fn parse(input: &str) -> Result { + match input.to_lowercase().as_str() { + "sha1" | "sha-1" => Ok(TimeOtpAlgorithm::Sha1), + "sha256" | "sha-256" => Ok(TimeOtpAlgorithm::Sha256), + "sha512" | "sha-512" => Ok(TimeOtpAlgorithm::Sha512), + _ => Err(()), + } } - } } -impl ToString for TimeOtpAlgorithm { - fn to_string(&self) -> String { - match self { +impl Display for TimeOtpAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { TimeOtpAlgorithm::Sha1 => "sha1".to_string(), TimeOtpAlgorithm::Sha256 => "sha256".to_string(), TimeOtpAlgorithm::Sha512 => "sha512".to_string(), - } + }) } } #[derive(Debug, Copy, Clone)] pub enum TimeOtpPeriod { - P30, - P60, - P120, + P30, + P60, + P120, } impl TimeOtpPeriod { - pub fn parse(input: &str) -> Result { - match input.to_lowercase().as_str() { - "30s" | "30" => Ok(TimeOtpPeriod::P30), - "1m" | "60s" | "60" => Ok(TimeOtpPeriod::P60), - "2m" | "120s" | "120" => Ok(TimeOtpPeriod::P120), - _ => Err(()), + pub fn parse(input: &str) -> Result { + match input.to_lowercase().as_str() { + "30s" | "30" => Ok(TimeOtpPeriod::P30), + "1m" | "60s" | "60" => Ok(TimeOtpPeriod::P60), + "2m" | "120s" | "120" => Ok(TimeOtpPeriod::P120), + _ => Err(()), + } } - } } -impl ToString for TimeOtpPeriod { - fn to_string(&self) -> String { - match self { - TimeOtpPeriod::P30 => "30".to_string(), - TimeOtpPeriod::P60 => "60".to_string(), - TimeOtpPeriod::P120 => "120".to_string(), - } +impl Display for TimeOtpPeriod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + TimeOtpPeriod::P30 => "30".to_string(), + TimeOtpPeriod::P60 => "60".to_string(), + TimeOtpPeriod::P120 => "120".to_string(), + } + ) } } #[derive(Debug)] pub struct TimeOtpOptions { - pub issuer: Option, + pub issuer: Option, - pub account: Option, + pub account: Option, - pub algorithm: TimeOtpAlgorithm, + pub algorithm: TimeOtpAlgorithm, - pub period: TimeOtpPeriod, + pub period: TimeOtpPeriod, - pub skew: bool, + pub skew: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct TimeOtpSettings { - #[serde(rename = "secret")] - pub secret: String, + #[serde(rename = "secret")] + pub secret: String, - #[serde(rename = "method")] - pub method: String, + #[serde(rename = "method")] + pub method: String, - #[serde(rename = "url")] - pub url: String, + #[serde(rename = "url")] + pub url: String, } #[derive(Debug, Serialize, Deserialize)] pub struct TimeOtpEnableBody { - #[serde(rename = "secret")] - pub secret: String, + #[serde(rename = "secret")] + pub secret: String, - #[serde(rename = "method")] - pub method: String, + #[serde(rename = "method")] + pub method: String, - #[serde(rename = "password")] - pub password: String, + #[serde(rename = "password")] + pub password: String, - #[serde(rename = "code")] - pub code: String, + #[serde(rename = "code")] + pub code: String, } #[derive(Debug, Serialize, Deserialize)] pub struct TfaDisableBody { - #[serde(rename = "code")] - pub code: String, + #[serde(rename = "code")] + pub code: String, } diff --git a/src/tools/request_upload.rs b/src/tools/request_upload.rs index ce5547f..45e282e 100644 --- a/src/tools/request_upload.rs +++ b/src/tools/request_upload.rs @@ -6,7 +6,10 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use tokio_sync_read_stream::SyncReadStream; -use crate::tools::{request_auth_confirmation_password, request_auth_confirmation_tfa, AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, AUTH_CONFIRMATION_TFA_HEADER_NAME}; +use crate::tools::{ + request_auth_confirmation_password, request_auth_confirmation_tfa, + AUTH_CONFIRMATION_PASSWORD_HEADER_NAME, AUTH_CONFIRMATION_TFA_HEADER_NAME, +}; use super::{super::models::*, ProgressReceiver}; use super::{get_session_from_uri, resolve_vault_api_uri, RequestError, SESSION_HEADER_NAME}; @@ -120,31 +123,67 @@ pub async fn do_multipart_upload_request_with_confirmation( debug: bool, progress_receiver: Arc>, ) -> Result { - let res = do_multipart_upload_request_internal(uri, path.clone(), field.clone(), file_path.clone(), debug, None, None, progress_receiver.clone()).await; + let res = do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path: path.clone(), + field: field.clone(), + file_path: file_path.clone(), + debug, + }, + None, + None, + progress_receiver.clone(), + ) + .await; match res { Ok(r) => Ok(r), - Err(err) => { - match err.clone() { - RequestError::Api { status, code, message: _ } => { - if status == 403 { - if code == "AUTH_CONFIRMATION_REQUIRED_TFA" { + Err(err) => match err.clone() { + RequestError::Api { + status, + code, + message: _, + } => { + if status == 403 { + if code == "AUTH_CONFIRMATION_REQUIRED_TFA" { let confirmation_tfa = request_auth_confirmation_tfa().await; - do_multipart_upload_request_internal(uri, path.clone(), field.clone(), file_path.clone(), debug, None, Some(confirmation_tfa), progress_receiver.clone()).await + do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path: path.clone(), + field: field.clone(), + file_path: file_path.clone(), + debug, + }, + None, + Some(confirmation_tfa), + progress_receiver.clone(), + ) + .await } else if code == "AUTH_CONFIRMATION_REQUIRED_PW" { let confirmation_pw = request_auth_confirmation_password().await; - do_multipart_upload_request_internal(uri, path.clone(), field.clone(), file_path.clone(), debug, Some(confirmation_pw), None, progress_receiver.clone()).await - } else { - Err(err) - } + do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path: path.clone(), + field: field.clone(), + file_path: file_path.clone(), + debug, + }, + Some(confirmation_pw), + None, + progress_receiver.clone(), + ) + .await } else { Err(err) } - }, - _ => { + } else { Err(err) } } + _ => Err(err), }, } } @@ -157,36 +196,52 @@ pub async fn do_multipart_upload_request( debug: bool, progress_receiver: Arc>, ) -> Result { - do_multipart_upload_request_internal(uri, path, field, file_path, debug, None, None, progress_receiver).await + do_multipart_upload_request_internal( + uri, + MultipartUploadRequestOptions { + path, + field, + file_path, + debug, + }, + None, + None, + progress_receiver, + ) + .await +} + +pub struct MultipartUploadRequestOptions { + pub path: String, + pub field: String, + pub file_path: String, + pub debug: bool, } pub async fn do_multipart_upload_request_internal( uri: &VaultURI, - path: String, - field: String, - file_path: String, - debug: bool, + options: MultipartUploadRequestOptions, confirmation_password: Option, confirmation_tfa: Option, progress_receiver: Arc>, ) -> Result { - let final_uri = resolve_vault_api_uri(uri.clone(), path); + let final_uri = resolve_vault_api_uri(uri.clone(), options.path); - if debug { - eprintln!("\rDEBUG: UPLOAD {file_path} -> {final_uri}"); + if options.debug { + eprintln!("\rDEBUG: UPLOAD {} -> {}", options.file_path, final_uri); } let client = reqwest::Client::new(); // Load file - let file_path_o = Path::new(&file_path); + let file_path_o = Path::new(&options.file_path); let file_name: String = match file_path_o.file_name() { Some(n) => n.to_str().unwrap_or("").to_string(), None => "".to_string(), }; - let file_res = File::open(&file_path); + let file_res = File::open(&options.file_path); let file_len: u64; let mut reporter: UploadProgressReporterSync; @@ -218,9 +273,10 @@ pub async fn do_multipart_upload_request_internal( let stream: SyncReadStream = reporter.clone().into(); - let file_part = reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name); + let file_part = + reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream)).file_name(file_name); - let form = reqwest::multipart::Form::new().part(field, file_part); + let form = reqwest::multipart::Form::new().part(options.field, file_part); let mut request_builder = client.post(final_uri).multipart(form); @@ -285,9 +341,7 @@ pub async fn do_multipart_upload_request_internal( Ok(res_body) } - Err(err) => { - Err(RequestError::NetworkError(err.to_string())) - } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } } @@ -363,8 +417,6 @@ pub async fn do_multipart_upload_request_memory( Ok(res_body) } - Err(err) => { - Err(RequestError::NetworkError(err.to_string())) - } + Err(err) => Err(RequestError::NetworkError(err.to_string())), } } From 3c6dbe010fa4057255a1025cdf767728d34aa480 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Thu, 19 Jun 2025 18:52:10 +0200 Subject: [PATCH 10/12] Update manual --- MANUAL.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/MANUAL.md b/MANUAL.md index 85d46ae..bd39e55 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -55,6 +55,7 @@ pmv-cli login [OPTIONS] | `-U, --username ` | Vault username. You can also specify the credentials in the URL | | `-D, --duration ` | Session duration. Can be: day, week, month or year | | `-I, --invite-code ` | Invite code. Setting this option will ignore the credentials and use the code | +| `-T, --tfa-code ` | Two factor authentication code | | `-h, --help` | Print help | ## Command: logout @@ -90,7 +91,12 @@ pmv-cli account | [context](#command-account-context) | Prints account context to the standard output | | [change-username](#command-account-change-username) | Changes username (only for root account) | | [change-password](#command-account-change-password) | Changes account password | -| [list](#command-account-list) | List accounts | +| [get-security-settings](#command-account-get-security-settings) | Gets account security settings | +| [set-auth-confirmation](#command-account-set-auth-confirmation) | Sets auth confirmation options | +| [get-totp-settings](#command-account-get-totp-settings) | Gets TOTP settings in order to enable two factor authentication | +| [enable-tfa](#command-account-enable-tfa) | Enables two factor authentication | +| [disable-tfa](#command-account-disable-tfa) | Disables two factor authentication | +| [list](#command-account-list) | Lists accounts | | [create](#command-account-create) | Creates new account | | [update](#command-account-update) | Updates an account | | [delete](#command-account-delete) | Deletes an existing account | @@ -155,9 +161,112 @@ pmv-cli account change-password | --- | --- | | `-h, --help` | Print help | +### Command: account get-security-settings + +Gets account security settings + +**Usage:** + +``` +pmv-cli account get-security-settings +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: account set-auth-confirmation + +Sets auth confirmation options + +**Usage:** + +``` +pmv-cli account set-auth-confirmation [OPTIONS] +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Set to 'true' to enable auth confirmation, Set it to 'false' to disable it | + +**Options:** + +| Option | Description | +| --- | --- | +| `--prefer-password` | | +| `Prefer using the account password instead of two factor authentication` | | +| `--period-seconds ` | | +| `Period (seconds) to remember the last auth confirmation` | | +| `-h, --help` | | +| `Print help` | | + +### Command: account get-totp-settings + +Gets TOTP settings in order to enable two factor authentication + +**Usage:** + +``` +pmv-cli account get-totp-settings [OPTIONS] +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `--issuer ` | TOTP issuer (to be added th the URL) | +| `--account ` | TOTP account (to be added th the URL) | +| `--algorithm ` | Hashing algorithm (sha-1, sha-256 or sha-512) | +| `--period ` | TOTP period (30s, 60s or 120s) | +| `--allow-skew` | Allows clock skew of 1 period | +| `-h, --help` | Print help | + +### Command: account enable-tfa + +Enables two factor authentication + +**Usage:** + +``` +pmv-cli account enable-tfa +``` + +**Arguments:** + +| Argument | Description | +| --- | --- | +| `` | Two factor authentication method (from the settings command result) | +| `` | Two factor authentication secret | + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + +### Command: account disable-tfa + +Disables two factor authentication + +**Usage:** + +``` +pmv-cli account disable-tfa +``` + +**Options:** + +| Option | Description | +| --- | --- | +| `-h, --help` | Print help | + ### Command: account list -List accounts +Lists accounts **Usage:** From f17aaf92b81f84599a08581992184af2c8786a19 Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sat, 21 Jun 2025 11:04:06 +0200 Subject: [PATCH 11/12] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c25a36d..9d04c6b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023-2024 Agustin San Roman +Copyright (c) 2023-2025 Agustin San Roman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 41cab90c4bae3e94a3aa82e3458106b04bccdfec Mon Sep 17 00:00:00 2001 From: AgustinSRG Date: Sat, 21 Jun 2025 11:48:42 +0200 Subject: [PATCH 12/12] Fix clippy warnings --- src/commands/media_download.rs | 4 ++-- src/tools/vault_uri.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/media_download.rs b/src/commands/media_download.rs index 1caf293..9f062c8 100644 --- a/src/commands/media_download.rs +++ b/src/commands/media_download.rs @@ -740,7 +740,7 @@ pub async fn download_media_asset( if path_parts.is_empty() { out_file = "download".to_string(); } else { - let last_part = path_parts.into_iter().last().unwrap_or("download"); + let last_part = path_parts.into_iter().next_back().unwrap_or("download"); out_file = last_part .split('?') .next() @@ -757,7 +757,7 @@ pub async fn download_media_asset( if path_parts.is_empty() { out_file = "download".to_string(); } else { - let last_part = path_parts.into_iter().last().unwrap_or("download"); + let last_part = path_parts.into_iter().next_back().unwrap_or("download"); out_file = last_part .split('?') .next() diff --git a/src/tools/vault_uri.rs b/src/tools/vault_uri.rs index 7ab2c45..a44916d 100644 --- a/src/tools/vault_uri.rs +++ b/src/tools/vault_uri.rs @@ -185,7 +185,7 @@ pub fn get_extension_from_url(download_path: &str, default_ext: &str) -> String if path_parts.is_empty() { default_ext.to_string() } else { - let last_part = path_parts.into_iter().last().unwrap_or("download"); + let last_part = path_parts.into_iter().next_back().unwrap_or("download"); let file_name = last_part .split('?') .next() @@ -193,7 +193,7 @@ pub fn get_extension_from_url(download_path: &str, default_ext: &str) -> String .to_string(); file_name .split('.') - .last() + .next_back() .unwrap_or(default_ext) .to_string() }