1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::config::{Config, ProviderConfig};
4use crate::error::ViaError;
5use crate::secrets::SecretValue;
6
7mod onepassword;
8
9pub trait SecretProvider {
10 fn resolve(&self, reference: &str) -> Result<SecretValue, ViaError>;
11}
12
13pub struct ProviderRegistry {
14 providers: BTreeMap<String, Box<dyn SecretProvider>>,
15}
16
17impl ProviderRegistry {
18 pub fn from_config(config: &Config) -> Result<Self, ViaError> {
19 let mut providers: BTreeMap<String, Box<dyn SecretProvider>> = BTreeMap::new();
20 for (name, provider) in &config.providers {
21 match provider {
22 ProviderConfig::OnePassword {
23 account,
24 cache,
25 cache_ttl_seconds,
26 } => {
27 providers.insert(
28 name.clone(),
29 Box::new(onepassword::OnePasswordCliProvider::new(
30 account.clone(),
31 *cache,
32 *cache_ttl_seconds,
33 provider_secret_references(config, name),
34 )),
35 );
36 }
37 }
38 }
39
40 Ok(Self { providers })
41 }
42
43 pub fn get(&self, name: &str) -> Result<&dyn SecretProvider, ViaError> {
44 self.providers
45 .get(name)
46 .map(|provider| provider.as_ref())
47 .ok_or_else(|| ViaError::InvalidConfig(format!("unknown provider `{name}`")))
48 }
49}
50
51fn provider_secret_references(config: &Config, provider_name: &str) -> Vec<String> {
52 config
53 .services
54 .values()
55 .filter(|service| service.provider == provider_name)
56 .flat_map(|service| service.secrets.values().cloned())
57 .collect::<BTreeSet<_>>()
58 .into_iter()
59 .collect()
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 fn config() -> Config {
67 Config::from_toml_str(
68 r#"
69version = 1
70
71[providers.onepassword]
72type = "1password"
73"#,
74 )
75 .unwrap()
76 }
77
78 #[test]
79 fn builds_registry_from_config() {
80 let registry = ProviderRegistry::from_config(&config()).unwrap();
81
82 assert!(registry.get("onepassword").is_ok());
83 }
84
85 #[test]
86 fn reports_missing_provider() {
87 let registry = ProviderRegistry::from_config(&config()).unwrap();
88 let error = match registry.get("missing") {
89 Ok(_) => panic!("expected missing provider error"),
90 Err(error) => error,
91 };
92
93 assert!(
94 matches!(error, ViaError::InvalidConfig(message) if message.contains("unknown provider"))
95 );
96 }
97}