use std::io::Read;
use anyhow::{Context, Result, bail};
use dialoguer::{Input, Password};
use crate::cli::PassCommand;
use crate::model::Credential;
use crate::{pick, secrets, store};
pub fn run(command: PassCommand) -> Result<()> {
match command {
PassCommand::Set { server, kind, login } => set(server, &kind, login),
PassCommand::Copy { server, kind, login, show } => copy(server, kind, login, show),
PassCommand::Show { server } => show(server),
PassCommand::List => list(),
PassCommand::Remove { server, kind, assume_yes } => remove(server, kind, assume_yes),
}
}
fn resolve(server: Option<String>, kind: Option<String>, prompt: &str) -> Result<(String, String)> {
let credentials = store::load_credentials()?;
match (server, kind) {
(Some(server), Some(kind)) => Ok((server, kind)),
(Some(server), None) => {
let matching: Vec<&Credential> = credentials.iter().filter(|c| c.server == server).collect();
match matching.as_slice() {
[] => bail!("no access saved for '{server}' - run `turnout pass set {server}`"),
[only] => Ok((server.clone(), only.kind.clone())),
_ => {
let owned: Vec<Credential> = matching.into_iter().cloned().collect();
pick::credential(&owned, None, prompt)
}
}
}
(None, kind) => pick::credential(&credentials, kind.as_deref(), prompt),
}
}
fn set(server: Option<String>, kind: &str, login: Option<String>) -> Result<()> {
let servers = store::load_servers()?;
if servers.is_empty() {
bail!("no servers in the catalog yet - run `turnout server add` first");
}
let interactive = pick::interactive();
let server = match server {
Some(server) => server,
None => pick::server(&servers, "Server")?,
};
if !servers.iter().any(|s| s.name == server) {
bail!("no server named '{server}' - see `turnout server list`");
}
let mut credentials = store::load_credentials()?;
let existing = credentials.iter().position(|c| c.server == server && c.kind == kind);
let login = match login {
Some(login) => login,
None => {
pick::ensure_interactive("--login is required")?;
let current = existing.map(|i| credentials[i].login.clone()).unwrap_or_default();
let mut input = Input::new().with_prompt("Login");
if !current.is_empty() {
input = input.default(current);
}
input.interact_text()?
}
};
let secret = if interactive {
Password::new()
.with_prompt(format!("Secret ({kind})"))
.with_confirmation("Repeat to confirm", "Values do not match")
.interact()?
} else {
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer).context("cannot read the secret from stdin")?;
buffer.trim_end_matches(['\r', '\n']).to_string()
};
if secret.is_empty() {
if interactive {
bail!("empty secret - nothing saved");
}
bail!("no secret on stdin - pipe it in, e.g. `echo \"$PASSWORD\" | turnout pass set {server} --kind {kind}`");
}
secrets::set(&server, kind, &secret)?;
match existing {
Some(index) => credentials[index].login = login,
None => credentials.push(Credential {
server: server.clone(),
kind: kind.to_string(),
login,
}),
}
credentials.sort_by(|a, b| (a.server.as_str(), a.kind.as_str()).cmp(&(b.server.as_str(), b.kind.as_str())));
store::save_credentials(&credentials)?;
println!("Access to '{server}' ({kind}) saved.");
Ok(())
}
fn copy(server: Option<String>, kind: Option<String>, login: bool, show: bool) -> Result<()> {
let (server, kind) = resolve(server, kind, "Copy access for")?;
let (server, kind) = (server.as_str(), kind.as_str());
let credentials = store::load_credentials()?;
let credential = credentials
.iter()
.find(|c| c.server == server && c.kind == kind)
.ok_or_else(|| anyhow::anyhow!("no access saved for '{server}' ({kind}) - run `turnout pass set {server}`"))?;
let (value, what) = if login {
(credential.login.clone(), "Login")
} else {
(secrets::get(server, kind)?, "Secret")
};
if show {
println!("{value}");
return Ok(());
}
arboard::Clipboard::new()
.and_then(|mut clipboard| clipboard.set_text(value))
.context("cannot access the clipboard (use --show to print instead)")?;
println!("{what} for '{server}' ({kind}) copied to the clipboard.");
Ok(())
}
fn show(server: Option<String>) -> Result<()> {
let credentials = store::load_credentials()?;
let server = match server {
Some(server) => server,
None => pick::credential(&credentials, None, "Show access for")?.0,
};
let server = server.as_str();
let matching: Vec<&Credential> = credentials.iter().filter(|c| c.server == server).collect();
if matching.is_empty() {
println!("No access saved for '{server}'.");
return Ok(());
}
println!("{server}");
for credential in matching {
println!(" {:10} login {}", credential.kind, credential.login);
}
println!("Secrets are stored in the OS keyring; copy with `turnout pass copy {server}`.");
Ok(())
}
fn list() -> Result<()> {
let credentials = store::load_credentials()?;
if credentials.is_empty() {
println!("No access saved yet - run `turnout pass set`.");
return Ok(());
}
let width = credentials.iter().map(|c| c.server.len()).max().unwrap_or(0);
for credential in credentials {
println!("{:width$} {:10} login {}", credential.server, credential.kind, credential.login);
}
Ok(())
}
fn remove(server: Option<String>, kind: Option<String>, assume_yes: bool) -> Result<()> {
let (server, kind) = resolve(server, kind, "Remove access for")?;
let (server, kind) = (server.as_str(), kind.as_str());
let mut credentials = store::load_credentials()?;
if !credentials.iter().any(|c| c.server == server && c.kind == kind) {
bail!("no access saved for '{server}' ({kind})");
}
let confirmed = pick::confirm_destructive(format!("Remove access to '{server}' ({kind})?"), assume_yes)?;
if !confirmed {
println!("Cancelled.");
return Ok(());
}
secrets::delete(server, kind)?;
credentials.retain(|c| !(c.server == server && c.kind == kind));
store::save_credentials(&credentials)?;
println!("Access to '{server}' ({kind}) removed.");
Ok(())
}