use std::path::{Path, PathBuf};
use vti_secrets::{SecretsConfig, create_seed_store};
const DEFAULT_DATA_DIR: &str = "./.trust-registry";
pub fn secrets_config_from_env() -> SecretsConfig {
let mut cfg = SecretsConfig::default();
let set = |name: &str| std::env::var(name).ok().filter(|v| !v.is_empty());
let set_any = |names: &[&str]| names.iter().find_map(|n| set(n));
let is_truthy = |v: String| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes");
cfg.seed = set("TR_SECRETS_SEED");
cfg.aws_secret_name = set("TR_SECRETS_AWS_SECRET_NAME");
cfg.aws_region = set("TR_SECRETS_AWS_REGION");
cfg.gcp_project = set("TR_SECRETS_GCP_PROJECT");
cfg.gcp_secret_name = set("TR_SECRETS_GCP_SECRET_NAME");
cfg.azure_vault_url = set("TR_SECRETS_AZURE_VAULT_URL");
cfg.azure_secret_name = set("TR_SECRETS_AZURE_SECRET_NAME");
if let Some(v) = set("TR_SECRETS_KEYRING_SERVICE") {
cfg.keyring_service = v;
}
cfg.vault_addr = set_any(&["VAULT_ADDR", "TR_SECRETS_VAULT_ADDR"]);
cfg.vault_namespace = set_any(&["VAULT_NAMESPACE", "TR_SECRETS_VAULT_NAMESPACE"]);
cfg.vault_secret_path = set("TR_SECRETS_VAULT_SECRET_PATH");
if let Some(v) = set("TR_SECRETS_VAULT_KV_MOUNT") {
cfg.vault_kv_mount = v;
}
if let Some(v) = set("TR_SECRETS_VAULT_SECRET_KEY") {
cfg.vault_secret_key = v;
}
if let Some(v) = set("TR_SECRETS_VAULT_AUTH_METHOD") {
cfg.vault_auth_method = v;
}
cfg.vault_k8s_role = set("TR_SECRETS_VAULT_K8S_ROLE");
if let Some(v) = set("TR_SECRETS_VAULT_K8S_MOUNT") {
cfg.vault_k8s_mount = v;
}
if let Some(v) = set("TR_SECRETS_VAULT_K8S_JWT_PATH") {
cfg.vault_k8s_jwt_path = v;
}
cfg.vault_token = set_any(&["VAULT_TOKEN", "TR_SECRETS_VAULT_TOKEN"]);
cfg.vault_approle_role_id = set("TR_SECRETS_VAULT_APPROLE_ROLE_ID");
cfg.vault_approle_secret_id = set("TR_SECRETS_VAULT_APPROLE_SECRET_ID");
if let Some(v) = set("TR_SECRETS_VAULT_APPROLE_MOUNT") {
cfg.vault_approle_mount = v;
}
cfg.vault_skip_verify = set_any(&["VAULT_SKIP_VERIFY", "TR_SECRETS_VAULT_SKIP_VERIFY"])
.map(is_truthy)
.unwrap_or(false);
cfg.k8s_secret_name = set("TR_SECRETS_K8S_SECRET_NAME");
cfg.k8s_namespace = set("TR_SECRETS_K8S_NAMESPACE");
if let Some(v) = set("TR_SECRETS_K8S_SECRET_KEY") {
cfg.k8s_secret_key = v;
}
cfg.allow_plaintext = std::env::var("TR_SECRETS_ALLOW_PLAINTEXT")
.map(|v| v == "true")
.unwrap_or(false);
cfg
}
pub fn backend_selected(cfg: &SecretsConfig) -> bool {
cfg.seed.is_some()
|| cfg.aws_secret_name.is_some()
|| cfg.gcp_secret_name.is_some()
|| cfg.azure_secret_name.is_some()
|| cfg.vault_secret_path.is_some()
|| cfg.k8s_secret_name.is_some()
}
pub fn data_dir() -> PathBuf {
std::env::var("TR_SECRETS_DATA_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(DEFAULT_DATA_DIR))
}
pub async fn write_profile(cfg: &SecretsConfig, dir: &Path, bundle: &str) -> Result<(), String> {
let store = create_seed_store(cfg, dir).map_err(|e| format!("secret store init: {e}"))?;
store
.set(bundle.as_bytes())
.await
.map_err(|e| format!("secret store write: {e}"))
}
pub async fn read_profile(cfg: &SecretsConfig, dir: &Path) -> Result<Option<String>, String> {
let store = create_seed_store(cfg, dir).map_err(|e| format!("secret store init: {e}"))?;
let bytes = store
.get()
.await
.map_err(|e| format!("secret store read: {e}"))?;
match bytes {
Some(bytes) => {
Ok(Some(String::from_utf8(bytes).map_err(|e| {
format!("secret store returned non-UTF8 bundle: {e}")
})?))
}
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
fn clear_env() {
for k in [
"TR_SECRETS_SEED",
"TR_SECRETS_AWS_SECRET_NAME",
"TR_SECRETS_GCP_SECRET_NAME",
"TR_SECRETS_AZURE_SECRET_NAME",
"VAULT_ADDR",
"VAULT_NAMESPACE",
"VAULT_TOKEN",
"VAULT_SKIP_VERIFY",
"TR_SECRETS_VAULT_ADDR",
"TR_SECRETS_VAULT_NAMESPACE",
"TR_SECRETS_VAULT_SECRET_PATH",
"TR_SECRETS_VAULT_KV_MOUNT",
"TR_SECRETS_VAULT_SECRET_KEY",
"TR_SECRETS_VAULT_AUTH_METHOD",
"TR_SECRETS_VAULT_K8S_ROLE",
"TR_SECRETS_VAULT_K8S_MOUNT",
"TR_SECRETS_VAULT_K8S_JWT_PATH",
"TR_SECRETS_VAULT_TOKEN",
"TR_SECRETS_VAULT_APPROLE_ROLE_ID",
"TR_SECRETS_VAULT_APPROLE_SECRET_ID",
"TR_SECRETS_VAULT_APPROLE_MOUNT",
"TR_SECRETS_VAULT_SKIP_VERIFY",
"TR_SECRETS_K8S_SECRET_NAME",
"TR_SECRETS_K8S_NAMESPACE",
"TR_SECRETS_K8S_SECRET_KEY",
"TR_SECRETS_KEYRING_SERVICE",
"TR_SECRETS_ALLOW_PLAINTEXT",
] {
unsafe { std::env::remove_var(k) };
}
}
#[test]
#[serial]
fn unconfigured_env_selects_no_explicit_backend() {
clear_env();
let cfg = secrets_config_from_env();
assert!(!backend_selected(&cfg));
}
#[test]
#[serial]
fn aws_secret_name_selects_backend() {
clear_env();
unsafe { std::env::set_var("TR_SECRETS_AWS_SECRET_NAME", "tr/profile") };
let cfg = secrets_config_from_env();
assert_eq!(cfg.aws_secret_name.as_deref(), Some("tr/profile"));
assert!(backend_selected(&cfg));
clear_env();
}
#[test]
#[serial]
fn vault_and_keyring_env_map_through() {
clear_env();
unsafe {
std::env::set_var("TR_SECRETS_VAULT_SECRET_PATH", "secret/tr");
std::env::set_var("TR_SECRETS_KEYRING_SERVICE", "trust-registry");
std::env::set_var("TR_SECRETS_ALLOW_PLAINTEXT", "true");
}
let cfg = secrets_config_from_env();
assert_eq!(cfg.vault_secret_path.as_deref(), Some("secret/tr"));
assert_eq!(cfg.keyring_service, "trust-registry");
assert!(cfg.allow_plaintext);
assert!(backend_selected(&cfg));
clear_env();
}
#[test]
#[serial]
fn vault_k8s_auth_env_maps_through() {
clear_env();
unsafe {
std::env::set_var("TR_SECRETS_VAULT_ADDR", "https://vault.svc:8200");
std::env::set_var("TR_SECRETS_VAULT_SECRET_PATH", "tr/master-seed");
std::env::set_var("TR_SECRETS_VAULT_AUTH_METHOD", "kubernetes");
std::env::set_var("TR_SECRETS_VAULT_K8S_ROLE", "trust-registry");
}
let cfg = secrets_config_from_env();
assert_eq!(cfg.vault_addr.as_deref(), Some("https://vault.svc:8200"));
assert_eq!(cfg.vault_secret_path.as_deref(), Some("tr/master-seed"));
assert_eq!(cfg.vault_auth_method, "kubernetes");
assert_eq!(cfg.vault_k8s_role.as_deref(), Some("trust-registry"));
assert_eq!(cfg.vault_kv_mount, "secret");
assert_eq!(cfg.vault_secret_key, "seed");
assert_eq!(cfg.vault_k8s_mount, "kubernetes");
assert!(backend_selected(&cfg));
clear_env();
}
#[test]
#[serial]
fn vault_canonical_env_names_take_precedence() {
clear_env();
unsafe {
std::env::set_var("VAULT_ADDR", "https://canonical:8200");
std::env::set_var("TR_SECRETS_VAULT_ADDR", "https://fallback:8200");
std::env::set_var("VAULT_TOKEN", "hvs.canonical");
std::env::set_var("VAULT_SKIP_VERIFY", "true");
std::env::set_var("TR_SECRETS_VAULT_SECRET_PATH", "tr/seed");
std::env::set_var("TR_SECRETS_VAULT_AUTH_METHOD", "token");
}
let cfg = secrets_config_from_env();
assert_eq!(cfg.vault_addr.as_deref(), Some("https://canonical:8200"));
assert_eq!(cfg.vault_token.as_deref(), Some("hvs.canonical"));
assert!(cfg.vault_skip_verify);
clear_env();
}
#[test]
#[serial]
fn vault_approle_and_k8s_secret_key_map_through() {
clear_env();
unsafe {
std::env::set_var("TR_SECRETS_VAULT_ADDR", "https://vault.example.com");
std::env::set_var("TR_SECRETS_VAULT_SECRET_PATH", "tr/seed");
std::env::set_var("TR_SECRETS_VAULT_AUTH_METHOD", "approle");
std::env::set_var("TR_SECRETS_VAULT_APPROLE_ROLE_ID", "role-123");
std::env::set_var("TR_SECRETS_VAULT_APPROLE_SECRET_ID", "secret-456");
std::env::set_var("TR_SECRETS_K8S_SECRET_KEY", "bip39_seed");
}
let cfg = secrets_config_from_env();
assert_eq!(cfg.vault_auth_method, "approle");
assert_eq!(cfg.vault_approle_role_id.as_deref(), Some("role-123"));
assert_eq!(cfg.vault_approle_secret_id.as_deref(), Some("secret-456"));
assert_eq!(cfg.k8s_secret_key, "bip39_seed");
clear_env();
}
#[test]
fn data_dir_defaults() {
let dir = data_dir();
assert!(dir.as_os_str().len() > 0);
}
}