use anyhow::{Context as _, Result};
use colored::Colorize;
use tsafe_bitwarden::{pull_items, BitwConfig};
use tsafe_core::{audit::AuditEntry, events::emit_event};
use crate::helpers::*;
use crate::cli::PullOnError;
const DEFAULT_PASSWORD_ENV: &str = "TSAFE_BW_PASSWORD";
#[allow(clippy::too_many_arguments)]
pub(crate) fn cmd_bitwarden_pull(
profile: &str,
api_url: Option<&str>,
identity_url: Option<&str>,
client_id: Option<&str>,
client_secret: Option<&str>,
folder: Option<&str>,
password_env: Option<&str>,
overwrite: bool,
on_error: PullOnError,
) -> Result<()> {
cmd_bitwarden_pull_ns(
profile,
api_url,
identity_url,
client_id,
client_secret,
folder,
password_env,
overwrite,
on_error,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn cmd_bitwarden_pull_ns(
profile: &str,
api_url: Option<&str>,
identity_url: Option<&str>,
client_id: Option<&str>,
client_secret: Option<&str>,
folder: Option<&str>,
password_env: Option<&str>,
overwrite: bool,
on_error: PullOnError,
ns: Option<&str>,
) -> Result<()> {
let cfg = resolve_config(api_url, identity_url, client_id, client_secret)?;
let pw_env = password_env.unwrap_or(DEFAULT_PASSWORD_ENV);
let items = match pull_items(&cfg, pw_env, folder).with_context(|| {
"failed to pull items from Bitwarden via the bw CLI\n\
\n Required env: TSAFE_BW_CLIENT_ID + TSAFE_BW_CLIENT_SECRET + TSAFE_BW_PASSWORD\
\n Install bw CLI: https://bitwarden.com/help/cli/\
\n Help: tsafe explain pull-auth"
}) {
Ok(items) => items,
Err(err) => match on_error {
PullOnError::FailAll => return Err(err),
PullOnError::SkipFailed | PullOnError::WarnOnly => {
eprintln!("{} Bitwarden pull failed: {err}", "!".yellow());
return Ok(());
}
},
};
if items.is_empty() {
println!(
"{} No Login items found in Bitwarden{}",
"i".blue(),
folder
.map(|f| format!(" (folder: {f})"))
.unwrap_or_default()
);
return Ok(());
}
let mut vault = open_vault(profile)?;
let mut imported = 0usize;
let mut skipped = 0usize;
for (raw_key, value) in &items {
let key = match ns {
Some(prefix) => format!("{prefix}.{raw_key}"),
None => raw_key.clone(),
};
let exists = vault.list().contains(&key.as_str());
if exists && !overwrite {
skipped += 1;
continue;
}
vault.set(&key, value, std::collections::HashMap::new())?;
imported += 1;
}
audit(profile)
.append(&AuditEntry::success(profile, "bw-pull", None))
.ok();
emit_event(profile, "bw-pull", None);
println!(
"{} Imported {imported} secret(s) from Bitwarden{}",
"✓".green(),
if skipped > 0 {
format!(" ({skipped} skipped — use --overwrite to replace)")
} else {
String::new()
}
);
Ok(())
}
fn resolve_config(
api_url: Option<&str>,
identity_url: Option<&str>,
client_id: Option<&str>,
client_secret: Option<&str>,
) -> Result<BitwConfig> {
let resolved_client_id = client_id
.map(|s| s.to_string())
.or_else(|| {
std::env::var("TSAFE_BW_CLIENT_ID")
.ok()
.filter(|s| !s.is_empty())
})
.ok_or_else(|| {
anyhow::anyhow!(
"Bitwarden client_id is not configured\n\
\n Fix: export TSAFE_BW_CLIENT_ID=<your-client-id>\
\n or set client_id in .tsafe.yml under the 'bw' pull source\
\n Help: tsafe explain pull-auth"
)
})?;
let resolved_client_secret = client_secret
.map(|s| s.to_string())
.or_else(|| {
std::env::var("TSAFE_BW_CLIENT_SECRET")
.ok()
.filter(|s| !s.is_empty())
})
.ok_or_else(|| {
anyhow::anyhow!(
"Bitwarden client_secret is not configured\n\
\n Fix: export TSAFE_BW_CLIENT_SECRET=<your-client-secret>\
\n or set client_secret in .tsafe.yml under the 'bw' pull source"
)
})?;
let resolved_api_url = api_url
.map(|s| s.to_string())
.or_else(|| {
std::env::var("TSAFE_BW_API_URL")
.ok()
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| BitwConfig::default_api_url().to_string());
let resolved_identity_url = identity_url
.map(|s| s.to_string())
.or_else(|| {
std::env::var("TSAFE_BW_IDENTITY_URL")
.ok()
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| BitwConfig::default_identity_url().to_string());
Ok(BitwConfig::new(
resolved_api_url,
resolved_identity_url,
resolved_client_id,
resolved_client_secret,
))
}