Skip to main content

arbiter_credential/
env_provider.rs

1//! Environment-variable credential provider.
2
3use 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
12/// A credential provider that resolves references from environment variables.
13pub struct EnvProvider {
14    /// Prefix used by `list_refs()` to discover credential env vars.
15    prefix: String,
16}
17
18impl EnvProvider {
19    /// Create a new env provider with the default prefix `ARBITER_CRED_`.
20    pub fn new() -> Self {
21        Self {
22            prefix: "ARBITER_CRED_".into(),
23        }
24    }
25
26    /// Create a new env provider with a custom prefix.
27    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        // SAFETY: test-only, no concurrent env access
82        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        // SAFETY: test-only, no concurrent env access
107        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        // SAFETY: test-only, no concurrent env access
133        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}