use std::io::Read;
use anyhow::Context;
use clap::{Args, Subcommand};
use microsandbox::{
RegistryAuth,
config::{
RegistryAuthEntry, RegistryCredentialStore, delete_registry_keyring_auth,
get_registry_keyring_auth, load_persisted_config_or_default, save_persisted_config,
set_registry_keyring_auth,
},
};
use crate::ui;
#[derive(Debug, Args)]
pub struct RegistryArgs {
#[command(subcommand)]
pub command: RegistryCommands,
}
#[derive(Debug, Subcommand)]
pub enum RegistryCommands {
Login(RegistryLoginArgs),
Logout(RegistryLogoutArgs),
#[command(visible_alias = "ls")]
List(RegistryListArgs),
}
#[derive(Debug, Args)]
pub struct RegistryLoginArgs {
pub registry: String,
#[arg(short, long)]
pub username: String,
#[arg(long)]
pub password_stdin: bool,
}
#[derive(Debug, Args)]
pub struct RegistryLogoutArgs {
pub registry: String,
}
#[derive(Debug, Args, Default)]
pub struct RegistryListArgs {}
pub async fn run(args: RegistryArgs) -> anyhow::Result<()> {
match args.command {
RegistryCommands::Login(args) => run_login(args),
RegistryCommands::Logout(args) => run_logout(args),
RegistryCommands::List(args) => run_list(args),
}
}
fn run_login(args: RegistryLoginArgs) -> anyhow::Result<()> {
let password = read_registry_password(args.password_stdin)?;
let previous_auth = get_registry_keyring_auth(&args.registry).ok().flatten();
set_registry_keyring_auth(&args.registry, &args.username, &password).map_err(|error| {
anyhow::anyhow!(
"secure credential storage is unavailable for `{}`: {}",
args.registry,
error
)
})?;
let mut config = load_persisted_config_or_default()?;
config.registries.auth.insert(
args.registry.clone(),
RegistryAuthEntry {
username: args.username,
store: Some(RegistryCredentialStore::Keyring),
password_env: None,
secret_name: None,
},
);
if let Err(error) = save_persisted_config(&config) {
let restore = match previous_auth {
Some(RegistryAuth::Basic { username, password }) => {
set_registry_keyring_auth(&args.registry, &username, &password)
}
Some(RegistryAuth::Anonymous) | None => delete_registry_keyring_auth(&args.registry),
};
if let Err(restore_error) = restore {
return Err(anyhow::anyhow!(
"{}; also failed to restore the previous keyring state for `{}`: {}",
error,
args.registry,
restore_error
));
}
return Err(error.into());
}
ui::success("Logged in", &args.registry);
Ok(())
}
fn run_logout(args: RegistryLogoutArgs) -> anyhow::Result<()> {
let mut config = load_persisted_config_or_default()?;
let previous_auth = get_registry_keyring_auth(&args.registry).ok().flatten();
let had_config_entry = config.registries.auth.remove(&args.registry).is_some();
let had_keyring_entry = previous_auth.is_some();
if !had_config_entry && !had_keyring_entry {
ui::warn(&format!(
"no stored registry credentials found for `{}`",
args.registry
));
return Ok(());
}
delete_registry_keyring_auth(&args.registry)?;
if let Err(error) = save_persisted_config(&config) {
if let Some(RegistryAuth::Basic { username, password }) = previous_auth {
let _ = set_registry_keyring_auth(&args.registry, &username, &password);
}
return Err(error.into());
}
ui::success("Logged out", &args.registry);
Ok(())
}
fn run_list(_args: RegistryListArgs) -> anyhow::Result<()> {
let config = load_persisted_config_or_default()?;
if config.registries.auth.is_empty() {
println!("No registries configured.");
return Ok(());
}
let mut entries: Vec<_> = config.registries.auth.iter().collect();
entries.sort_by_key(|(name, _)| *name);
let mut table = ui::Table::new(&["REGISTRY", "USERNAME", "SOURCE"]);
for (registry, entry) in entries {
table.add_row(vec![
registry.clone(),
entry.username.clone(),
credential_source_label(entry).to_string(),
]);
}
table.print();
Ok(())
}
fn read_registry_password(password_stdin: bool) -> anyhow::Result<String> {
let password = if password_stdin {
let mut password = String::new();
std::io::stdin()
.read_to_string(&mut password)
.context("failed to read password from stdin")?;
password.trim_end_matches(['\r', '\n']).to_string()
} else {
rpassword::prompt_password("Password/token: ")
.context("failed to read password from the terminal")?
};
if password.is_empty() {
anyhow::bail!("password/token cannot be empty");
}
Ok(password)
}
fn credential_source_label(entry: &RegistryAuthEntry) -> &'static str {
let source_count = usize::from(entry.store.is_some())
+ usize::from(entry.password_env.is_some())
+ usize::from(entry.secret_name.is_some());
if source_count > 1 {
return "multiple";
}
match (
entry.store,
entry.password_env.as_ref(),
entry.secret_name.as_ref(),
) {
(Some(RegistryCredentialStore::Keyring), _, _) => "keyring",
(None, Some(_), None) => "password_env",
(None, None, Some(_)) => "secret_name",
_ => "unset",
}
}