use std::io::Write;
use std::path::Path;
use anyhow::{Context as _, anyhow, bail};
use serde_json::{Value, json};
use trusty_secrets::api::methods::{
CopyResponse, DeleteResponse, ListResponse, ScopesResponse, SetOutcome, SetResponse, method,
};
use trusty_secrets::store::parse_dotenv;
use trusty_secrets::{BackendId, SecretKey, SecretValue};
use super::Ctx;
use super::session::{check_group, entry_key};
const ARGV_REFUSED: &str =
"tm secrets set: the value never goes on the command line; copy it to the clipboard";
const VALUE_FLAG_REFUSED: &str =
"tm secrets set: `--value` accepts only `-` (read stdin); a value is never an argument";
pub(super) struct SetArgs<'a> {
pub(super) key: &'a str,
pub(super) group: Option<&'a str>,
pub(super) value: Option<&'a str>,
pub(super) extra: bool,
}
fn outcome_word(outcome: SetOutcome) -> &'static str {
match outcome {
SetOutcome::New => "new",
SetOutcome::Updated => "updated",
_ => "stored",
}
}
pub(super) async fn set(
ctx: &Ctx<'_>,
args: SetArgs<'_>,
out: &mut dyn Write,
) -> anyhow::Result<()> {
if args.extra {
bail!(ARGV_REFUSED);
}
let key = entry_key(args.key, args.group)?;
let (raw, empty) = match args.value {
None => (ctx.clipboard.read()?, "the clipboard is empty"),
Some("-") => (ctx.stdin.read()?, "stdin is empty"),
Some(_) => bail!(VALUE_FLAG_REFUSED),
};
let value = SecretValue::new(raw.trim());
drop(raw);
if value.is_empty() {
bail!("tm secrets set: {empty}; nothing was stored");
}
let vault = ctx.project_vault().await?;
let params = json!({ "vault": vault, "key": key, "value": value });
let response: SetResponse = ctx.call(method::SET, params).await?;
let word = outcome_word(response.outcome);
writeln!(out, "({word}) secret set {key}: {}", response.masked)?;
Ok(())
}
pub(super) async fn list(ctx: &Ctx<'_>, out: &mut dyn Write) -> anyhow::Result<()> {
let scopes: ScopesResponse = ctx.call(method::SCOPES, Value::Null).await?;
for scope in scopes.scopes {
let listed: ListResponse = ctx
.call(method::LIST, json!({ "vault": scope.vault }))
.await?;
writeln!(out, "{} ({:?}):", listed.vault, scope.kind)?;
if listed.keys.is_empty() {
writeln!(out, " (no keys)")?;
}
for meta in listed.keys {
writeln!(out, " {}", meta.name)?;
}
}
Ok(())
}
fn names(keys: &[SecretKey]) -> String {
keys.iter()
.map(SecretKey::as_str)
.collect::<Vec<_>>()
.join(", ")
}
pub(super) async fn remove(
ctx: &Ctx<'_>,
key: &str,
group: Option<&str>,
out: &mut dyn Write,
) -> anyhow::Result<()> {
let key = entry_key(key, group)?;
let vault = ctx.project_vault().await?;
let response: DeleteResponse = ctx
.call(method::DELETE, json!({ "vault": vault, "key": key }))
.await?;
if !response.removed {
bail!("tm secrets remove: {key} is not in {vault}");
}
writeln!(out, "removed {key} from {vault}")?;
Ok(())
}
pub(super) async fn import(
ctx: &Ctx<'_>,
path: &Path,
group: Option<&str>,
out: &mut dyn Write,
) -> anyhow::Result<()> {
check_group(group)?;
let shown = path.display();
let text = std::fs::read_to_string(path)
.with_context(|| format!("tm secrets import: cannot read {shown}"))?;
let entries = parse_dotenv(&text).map_err(|e| anyhow!("tm secrets import: {shown}: {e}"))?;
drop(text);
let vault = ctx.project_vault().await?;
let (mut imported, mut skipped, mut failed) = (0usize, 0usize, Vec::new());
for entry in &entries {
let key = match entry_key(entry.name(), group) {
Ok(key) => key,
Err(e) => {
writeln!(out, "failed {}: {e}", entry.name())?;
failed.push(entry.name().to_owned());
continue;
}
};
if entry.is_reference() {
writeln!(out, "skipped {key} (a secret:// reference)")?;
skipped += 1;
continue;
}
let value = SecretValue::new(entry.raw());
let params = json!({ "vault": vault, "key": key, "value": value });
match ctx.call::<SetResponse>(method::SET, params).await {
Ok(response) => {
writeln!(out, "imported {key} ({})", outcome_word(response.outcome))?;
imported += 1;
}
Err(e) => {
writeln!(out, "failed {key}: {e}")?;
failed.push(key.to_string());
}
}
}
let count = failed.len();
writeln!(
out,
"{vault}: {imported} imported, {skipped} skipped, {count} failed"
)?;
if count > 0 {
bail!(
"tm secrets import: {count} key(s) failed: {}",
failed.join(", ")
);
}
Ok(())
}
pub(super) async fn copy(
ctx: &Ctx<'_>,
from: &str,
to: &str,
keys: &[&str],
out: &mut dyn Write,
) -> anyhow::Result<()> {
let backend = |id: &str| BackendId::new(id).map_err(|e| anyhow!("tm secrets copy: {e}"));
let (from, to) = (backend(from)?, backend(to)?);
let keys = keys
.iter()
.map(|key| SecretKey::new(key).map_err(|e| anyhow!("tm secrets copy: {e}")))
.collect::<anyhow::Result<Vec<_>>>()?;
let params = json!({ "from_backend": from, "to_backend": to, "keys": keys });
let response: CopyResponse = ctx.call(method::COPY, params).await?;
for key in &response.copied {
writeln!(out, "copied {key}")?;
}
for key in &response.failed {
writeln!(out, "not copied {key}")?;
}
let (copied, failed) = (response.copied.len(), response.failed.len());
writeln!(out, "{from} -> {to}: {copied} copied, {failed} not copied")?;
if failed > 0 {
bail!(
"tm secrets copy: {failed} key(s) not copied (absent from {from}, or refused by {to}): {}",
names(&response.failed)
);
}
Ok(())
}