diff --git a/src/commands/config.rs b/src/commands/config.rs index 31103da3..03e759bf 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -21,11 +21,11 @@ pub struct ConfigCommand { /// Ignore federation hints. #[clap(long)] - pub ignore_federation_hints: bool, + pub ignore_federation_hints: Option, /// Auto accept federation hints. #[clap(long)] - pub auto_accept_federation_hints: bool, + pub auto_accept_federation_hints: Option, /// Overwrite the existing configuration file. #[clap(long)] @@ -53,58 +53,104 @@ impl ConfigCommand { .path .map(Ok) .unwrap_or_else(Config::default_config_path)?; + let cwd = std::env::current_dir().context("failed to determine current directory")?; - if !self.overwrite && path.is_file() { - bail!( - "configuration file `{path}` already exists; use `--overwrite` to overwrite it", + if self.overwrite && path.is_file() { + println!( + "Overwriting configuration file: `{path}`", path = path.display() ); } - let home_url = &self - .common - .registry - .clone() - .map(RegistryUrl::new) - .transpose()? - .map(|u| u.to_string()); + let mut changing_home_registry = false; + + let config = if self.overwrite { + let home_url = self + .common + .registry + .as_ref() + .map(RegistryUrl::new) + .transpose()? + .map(|u| u.to_string()) + .ok_or(anyhow::anyhow!( + "Please configure your home registry: warg config --registry " + ))?; + + changing_home_registry = true; + + Config { + home_url: Some(home_url), + registries_dir: self.registries_dir.map(|p| cwd.join(p)), + content_dir: self.content_dir.map(|p| cwd.join(p)), + namespace_map_path: self.namespace_path.map(|p| cwd.join(p)), + keys: self.common.read_config()?.keys, + keyring_auth: false, + ignore_federation_hints: self.ignore_federation_hints.unwrap_or_default(), + auto_accept_federation_hints: self.auto_accept_federation_hints.unwrap_or_default(), + disable_interactive: false, + keyring_backend: self.keyring_backend, + } + } else { + let mut config = self.common.read_config()?; + if self.common.registry.is_some() { + let home_url = self + .common + .registry + .as_ref() + .map(RegistryUrl::new) + .transpose()? + .map(|u| u.to_string()); + if home_url != config.home_url { + changing_home_registry = true; + config.home_url = home_url; + } + } + if config.home_url.is_none() { + bail!("Please configure your home registry: warg config --registry "); + } + if self.registries_dir.is_some() { + config.registries_dir = self.registries_dir.map(|p| cwd.join(p)); + } + if self.content_dir.is_some() { + config.content_dir = self.content_dir.map(|p| cwd.join(p)); + } + if self.namespace_path.is_some() { + config.namespace_map_path = self.namespace_path.map(|p| cwd.join(p)); + } + if let Some(ignore_federation_hints) = self.ignore_federation_hints { + config.ignore_federation_hints = ignore_federation_hints; + } + if let Some(auto_accept_federation_hints) = self.auto_accept_federation_hints { + config.auto_accept_federation_hints = auto_accept_federation_hints; + } + if self.keyring_backend.is_some() { + config.keyring_backend = self.keyring_backend; + } + + config + }; // The paths specified on the command line are relative to the current // directory. // // `write_to_file` will handle normalizing the paths to be relative to // the configuration file's directory. - let cwd = std::env::current_dir().context("failed to determine current directory")?; - let config = Config { - home_url: home_url.clone(), - registries_dir: self.registries_dir.map(|p| cwd.join(p)), - content_dir: self.content_dir.map(|p| cwd.join(p)), - namespace_map_path: self.namespace_path.map(|p| cwd.join(p)), - keys: self.common.read_config()?.keys, - keyring_auth: false, - ignore_federation_hints: self.ignore_federation_hints, - auto_accept_federation_hints: self.auto_accept_federation_hints, - disable_interactive: false, - keyring_backend: self.keyring_backend.clone(), - }; - config.write_to_file(&path)?; // reset when changing home registry - let client = self.common.create_client(&config)?; - client.reset_namespaces().await?; - client.reset_registry().await?; + if changing_home_registry { + let client = self.common.create_client(&config)?; + client.reset_namespaces().await?; + client.reset_registry().await?; + } - println!( - "created warg configuration file `{path}`", - path = path.display(), - ); + println!("Set configuration file `{path}`", path = path.display(),); Ok(()) } } -fn keyring_backend_parser(s: &str) -> Result { +pub(crate) fn keyring_backend_parser(s: &str) -> Result { if Keyring::SUPPORTED_BACKENDS.contains(&s) { Ok(s.to_string()) } else { @@ -112,7 +158,7 @@ fn keyring_backend_parser(s: &str) -> Result { } } -fn keyring_backend_help() -> clap::builder::StyledStr { +pub(crate) fn keyring_backend_help() -> clap::builder::StyledStr { use std::fmt::Write as _; let mut help = String::new(); diff --git a/src/commands/download.rs b/src/commands/download.rs index af810a25..7b750ee3 100644 --- a/src/commands/download.rs +++ b/src/commands/download.rs @@ -30,7 +30,7 @@ impl DownloadCommand { let config = self.common.read_config()?; let client = self.common.create_client(&config)?; - println!("downloading package `{name}`...", name = self.name); + println!("Downloading `{name}`...", name = self.name); // if user specifies exact verion, then set the `VersionReq` to exact match let version = match &self.version { @@ -47,8 +47,7 @@ impl DownloadCommand { })?; println!( - "Downloaded version {version} of package `{name}` ({digest}) to local cache", - name = self.name, + "Downloaded version: {version}\nDigest: {digest}\n", version = download.version, digest = download.digest ); diff --git a/src/commands/info.rs b/src/commands/info.rs index 7481d90d..8c3ba826 100644 --- a/src/commands/info.rs +++ b/src/commands/info.rs @@ -2,6 +2,7 @@ use super::CommonOptions; use anyhow::Result; use clap::{ArgAction, Args}; use warg_client::{ + keyring::Keyring, storage::{ContentStorage, NamespaceMapStorage, PackageInfo, RegistryStorage}, Client, }; @@ -30,13 +31,29 @@ impl InfoCommand { let config = self.common.read_config()?; let client = self.common.create_client(&config)?; - println!("registry: {url}", url = client.url()); - println!("\npackages in client storage:"); + println!("\nRegistry: {url}", url = client.url()); + if config.keyring_auth + && Keyring::from_config(&config)? + .get_auth_token(client.url())? + .is_some() + { + println!( + "(Using credentials{keyring_backend})", + keyring_backend = if let Some(keyring_backend) = &config.keyring_backend { + format!(" stored in `{keyring_backend}` keyring backend") + } else { + "".to_string() + } + ); + } else { + println!("(Not logged in)"); + } + println!("\nPackages in client storage:"); match self.package { Some(package) => { let info = client.package(&package).await?; if let Some(registry) = client.get_warg_registry(package.namespace()).await? { - println!("registry: {registry}"); + println!("Registry: {registry}"); } Self::print_package_info(&info); } @@ -47,24 +64,26 @@ impl InfoCommand { .await? .iter() .for_each(|(registry, packages)| { - println!("registry: {registry}"); + println!("\nRegistry: {registry}"); packages.iter().for_each(Self::print_package_info); }); } } if self.namespaces { - println!("\nnamespace mappings in client storage"); + println!("\nNamespace mappings in client storage"); Self::print_namespace_map(&client).await?; return Ok(()); } + println!(); + Ok(()) } fn print_package_info(info: &PackageInfo) { - println!(" name: {name}", name = info.name); - println!(" versions:"); + println!(" Name: {name}", name = info.name); + println!(" Versions:"); info.state.releases().for_each(|r| { if let Some(content) = r.content() { Self::print_release(&r.version, content); diff --git a/src/commands/login.rs b/src/commands/login.rs index fe218c98..905ff1dd 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -1,6 +1,7 @@ +use crate::commands::config::{keyring_backend_help, keyring_backend_parser}; use anyhow::{bail, Context, Result}; use clap::Args; -use dialoguer::{theme::ColorfulTheme, Password}; +use dialoguer::{theme::ColorfulTheme, Confirm, Password}; use p256::ecdsa::SigningKey; use rand_core::OsRng; use warg_client::keyring::Keyring; @@ -15,10 +16,6 @@ pub struct LoginCommand { #[clap(flatten)] pub common: CommonOptions, - /// The subcommand to execute. - #[clap(flatten)] - keyring_entry: KeyringEntryArgs, - /// Ignore federation hints. #[clap(long)] pub ignore_federation_hints: bool, @@ -26,80 +23,99 @@ pub struct LoginCommand { /// Auto accept federation hints. #[clap(long)] pub auto_accept_federation_hints: bool, -} -#[derive(Args)] -struct KeyringEntryArgs { - /// The URL of the registry to store an auth token for. - #[clap(value_name = "URL")] - pub url: Option, -} - -impl KeyringEntryArgs { - fn set_entry(&self, keyring: &Keyring, home_url: Option, token: &str) -> Result<()> { - if let Some(url) = &self.url { - keyring.set_auth_token(url, token)?; - } else if let Some(url) = &home_url { - keyring.set_auth_token(&RegistryUrl::new(url)?, token)?; - } else { - bail!("Please configure your home registry: warg config --registry ") - } - Ok(()) - } + /// The backend to use for keyring access + #[clap(long, value_name = "KEYRING_BACKEND", value_parser = keyring_backend_parser, long_help = keyring_backend_help())] + pub keyring_backend: Option, } impl LoginCommand { /// Executes the command. pub async fn exec(self) -> Result<()> { - let home_url = &self + let mut config = self.common.read_config()?; + let mut registry_url = &self .common .registry - .clone() + .as_ref() .map(RegistryUrl::new) .transpose()? .map(|u| u.to_string()); - let mut config = self.common.read_config()?; config.ignore_federation_hints = self.ignore_federation_hints; config.auto_accept_federation_hints = self.auto_accept_federation_hints; - let keyring = Keyring::from_config(&config)?; - if home_url.is_some() { - config.home_url.clone_from(home_url); + // set keyring backend, if specified + if self.keyring_backend.is_some() { + config.keyring_backend = self.keyring_backend; + } + + if registry_url.is_none() && config.home_url.is_none() { + bail!("Please set your registry: warg login --registry "); + } + + if registry_url.is_some() + && registry_url != &config.home_url + && Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Set `{registry}` as your home (or default) registry?", + registry = registry_url.as_deref().unwrap(), + )) + .default(true) + .interact()? + { + config.home_url.clone_from(registry_url); config.write_to_file(&Config::default_config_path()?)?; // reset if changing home registry let client = self.common.create_client(&config)?; client.reset_namespaces().await?; client.reset_registry().await?; + } else if registry_url.is_none() { + registry_url = &config.home_url; } + let keyring = Keyring::from_config(&config)?; config.keyring_auth = true; + let prompt = format!( + "Enter auth token for registry: {registry}", + registry = registry_url.as_deref().unwrap() + ); + if config.keys.is_empty() { config.keys.insert("default".to_string()); let key = SigningKey::random(&mut OsRng).into(); - keyring.set_signing_key(None, &key, &mut config.keys, config.home_url.as_deref())?; + keyring.set_signing_key(None, &key, &mut config.keys, registry_url.as_deref())?; let public_key = key.public_key(); let token = Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter auth token") + .with_prompt(prompt) .interact() .context("failed to read token")?; - self.keyring_entry - .set_entry(&keyring, self.common.read_config()?.home_url, &token)?; + keyring.set_auth_token(&RegistryUrl::new(registry_url.as_deref().unwrap())?, &token)?; config.write_to_file(&Config::default_config_path()?)?; - println!("auth token was set successfully, and generated default key",); + println!("Auth token was set successfully, and generated default key."); println!("Public Key: {public_key}"); return Ok(()); } let token = Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter auth token") + .with_prompt(prompt) .interact() .context("failed to read token")?; - self.keyring_entry - .set_entry(&keyring, self.common.read_config()?.home_url, &token)?; + keyring.set_auth_token(&RegistryUrl::new(registry_url.as_deref().unwrap())?, &token)?; config.write_to_file(&Config::default_config_path()?)?; - println!("auth token was set successfully",); + println!("Auth token was set successfully."); + + if let Ok(private_key) = keyring.get_signing_key( + self.common.registry.as_deref(), + &config.keys, + registry_url.as_deref(), + ) { + println!("\nSigning key is still available:"); + let public_key = private_key.public_key(); + println!("Key ID: {}", public_key.fingerprint()); + println!("Public Key: {public_key}"); + } + Ok(()) } } diff --git a/src/commands/logout.rs b/src/commands/logout.rs index 4d0c99b0..505a37c6 100644 --- a/src/commands/logout.rs +++ b/src/commands/logout.rs @@ -1,7 +1,6 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use clap::Args; -use warg_client::{Config, RegistryUrl}; -use warg_credentials::keyring::Keyring; +use warg_client::{keyring::Keyring, Config, RegistryUrl}; use super::CommonOptions; @@ -11,41 +10,37 @@ pub struct LogoutCommand { /// The common command options. #[clap(flatten)] pub common: CommonOptions, - /// The subcommand to execute. - #[clap(flatten)] - keyring_entry: KeyringEntryArgs, -} - -#[derive(Args)] -struct KeyringEntryArgs { - /// The URL of the registry to delete an auth token for. - #[clap(value_name = "URL")] - pub url: Option, -} - -impl KeyringEntryArgs { - fn delete_entry(&self, keyring: &Keyring, home_url: Option) -> Result<()> { - if let Some(url) = &self.url { - keyring.delete_auth_token(url)?; - } else if let Some(url) = &home_url { - keyring.delete_auth_token(&RegistryUrl::new(url)?)?; - } else { - bail!("Please configure your home registry: warg config --registry ") - } - Ok(()) - } } impl LogoutCommand { /// Executes the command. pub async fn exec(self) -> Result<()> { let mut config = self.common.read_config()?; + let registry_url = &self + .common + .registry + .as_deref() + .or(config.home_url.as_deref()) + .map(RegistryUrl::new) + .transpose()? + .ok_or(anyhow::anyhow!( + "Registry is not specified, so nothing to logout." + ))?; let keyring = Keyring::from_config(&config)?; - self.keyring_entry - .delete_entry(&keyring, config.home_url.clone())?; - config.keyring_auth = false; - config.write_to_file(&Config::default_config_path()?)?; - println!("auth token was deleted successfully",); + keyring.delete_auth_token(registry_url)?; + let registry_url_str = registry_url.to_string(); + if config + .home_url + .as_deref() + .is_some_and(|home_url| home_url == registry_url_str) + { + config.keyring_auth = false; + config.write_to_file(&Config::default_config_path()?)?; + } + println!( + "Logged out of registry: {registry}", + registry = registry_url_str + ); Ok(()) } }