arbiter_credential/
env_provider.rs1use std::env;
4
5use async_trait::async_trait;
6use secrecy::SecretString;
7use tracing::{debug, warn};
8
9use crate::error::CredentialError;
10use crate::provider::{CredentialProvider, CredentialRef};
11
12pub struct EnvProvider {
14 prefix: String,
16}
17
18impl EnvProvider {
19 pub fn new() -> Self {
21 Self {
22 prefix: "ARBITER_CRED_".into(),
23 }
24 }
25
26 pub fn with_prefix(prefix: impl Into<String>) -> Self {
28 Self {
29 prefix: prefix.into(),
30 }
31 }
32}
33
34impl Default for EnvProvider {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40#[async_trait]
41impl CredentialProvider for EnvProvider {
42 async fn resolve(&self, reference: &str) -> Result<SecretString, CredentialError> {
43 debug!(reference, "resolving credential from environment");
44
45 if !reference.starts_with(&self.prefix) {
46 return Err(CredentialError::NotFound(format!(
47 "credential reference '{}' does not match required prefix '{}'",
48 reference, self.prefix
49 )));
50 }
51
52 env::var(reference).map(SecretString::from).map_err(|e| {
53 warn!(reference, error = %e, "env var not found");
54 CredentialError::NotFound(format!("env var {reference}: {e}"))
55 })
56 }
57
58 async fn list_refs(&self) -> Result<Vec<CredentialRef>, CredentialError> {
59 let refs: Vec<CredentialRef> = env::vars()
60 .filter(|(key, _)| key.starts_with(&self.prefix))
61 .map(|(key, _)| CredentialRef {
62 name: key,
63 provider: "env".into(),
64 last_rotated: None,
65 })
66 .collect();
67
68 debug!(count = refs.len(), prefix = %self.prefix, "listed env credential refs");
69 Ok(refs)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use secrecy::ExposeSecret;
77
78 #[tokio::test]
79 async fn resolves_env_var() {
80 let key = "ARBITER_CRED_TEST_RESOLVE_42";
81 unsafe { env::set_var(key, "secret-value") };
83
84 let provider = EnvProvider::new();
85 let value = provider.resolve(key).await.unwrap();
86 assert_eq!(value.expose_secret(), "secret-value");
87
88 unsafe { env::remove_var(key) };
89 }
90
91 #[tokio::test]
92 async fn missing_env_var_is_not_found() {
93 let provider = EnvProvider::new();
94 let err = provider
95 .resolve("ARBITER_CRED_DEFINITELY_DOES_NOT_EXIST_XYZ")
96 .await
97 .unwrap_err();
98 assert!(matches!(err, CredentialError::NotFound(_)));
99 }
100
101 #[tokio::test]
102 async fn list_refs_filters_by_prefix() {
103 let key1 = "ARBITER_CRED_LIST_TEST_A";
104 let key2 = "ARBITER_CRED_LIST_TEST_B";
105 let key3 = "UNRELATED_VAR_LIST_TEST";
106 unsafe {
108 env::set_var(key1, "a");
109 env::set_var(key2, "b");
110 env::set_var(key3, "c");
111 }
112
113 let provider = EnvProvider::new();
114 let refs = provider.list_refs().await.unwrap();
115
116 let names: Vec<_> = refs.iter().map(|r| r.name.as_str()).collect();
117 assert!(names.contains(&key1));
118 assert!(names.contains(&key2));
119 assert!(!names.contains(&key3));
120 assert!(refs.iter().all(|r| r.provider == "env"));
121
122 unsafe {
123 env::remove_var(key1);
124 env::remove_var(key2);
125 env::remove_var(key3);
126 }
127 }
128
129 #[tokio::test]
130 async fn custom_prefix() {
131 let key = "MY_PREFIX_KEY_1";
132 unsafe { env::set_var(key, "value") };
134
135 let provider = EnvProvider::with_prefix("MY_PREFIX_");
136 let refs = provider.list_refs().await.unwrap();
137 let names: Vec<_> = refs.iter().map(|r| r.name.as_str()).collect();
138 assert!(names.contains(&key));
139
140 unsafe { env::remove_var(key) };
141 }
142}