Skip to main content

neuron_secret_env/
lib.rs

1#![deny(missing_docs)]
2//! Secret resolver that reads from process environment variables.
3//!
4//! Uses `SecretSource::Custom { provider: "env", config: { "var_name": "..." } }`.
5
6use async_trait::async_trait;
7use layer0::secret::SecretSource;
8use neuron_secret::{SecretError, SecretLease, SecretResolver, SecretValue};
9
10/// Resolves secrets from process environment variables.
11///
12/// Uses `SecretSource::Custom { provider: "env", config: { "var_name": "..." } }`.
13///
14/// Config schema:
15/// ```json
16/// { "type": "custom", "provider": "env", "config": { "var_name": "ANTHROPIC_API_KEY" } }
17/// ```
18///
19/// Required config field: `var_name` (string) — the environment variable name to read.
20pub struct EnvResolver;
21
22#[async_trait]
23impl SecretResolver for EnvResolver {
24    async fn resolve(&self, source: &SecretSource) -> Result<SecretLease, SecretError> {
25        match source {
26            SecretSource::Custom { provider, config } if provider == "env" => {
27                let var_name =
28                    config
29                        .get("var_name")
30                        .and_then(|v| v.as_str())
31                        .ok_or_else(|| {
32                            SecretError::NotFound("env source requires config.var_name".into())
33                        })?;
34                match std::env::var(var_name) {
35                    Ok(val) => Ok(SecretLease::permanent(SecretValue::new(val.into_bytes()))),
36                    Err(_) => Err(SecretError::NotFound(format!("env var {var_name} not set"))),
37                }
38            }
39            _ => Err(SecretError::NoResolver("env".into())),
40        }
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use std::sync::Arc;
48
49    fn _assert_send_sync<T: Send + Sync>() {}
50
51    #[test]
52    fn object_safety() {
53        _assert_send_sync::<Box<dyn SecretResolver>>();
54        _assert_send_sync::<Arc<dyn SecretResolver>>();
55        let _: Box<dyn SecretResolver> = Box::new(EnvResolver);
56        let _: Arc<dyn SecretResolver> = Arc::new(EnvResolver);
57    }
58
59    #[tokio::test]
60    async fn resolves_set_env_var() {
61        // SAFETY: test-only; unique var name avoids cross-test interference.
62        unsafe { std::env::set_var("NEURON_TEST_SECRET_ENV", "test-value-42") };
63        let resolver = EnvResolver;
64        let source = SecretSource::Custom {
65            provider: "env".into(),
66            config: serde_json::json!({ "var_name": "NEURON_TEST_SECRET_ENV" }),
67        };
68        let lease = resolver.resolve(&source).await.unwrap();
69        lease.value.with_bytes(|b| assert_eq!(b, b"test-value-42"));
70        // SAFETY: test-only cleanup.
71        unsafe { std::env::remove_var("NEURON_TEST_SECRET_ENV") };
72    }
73
74    #[tokio::test]
75    async fn rejects_missing_env_var() {
76        // SAFETY: test-only; unique var name avoids cross-test interference.
77        unsafe { std::env::remove_var("NEURON_TEST_MISSING_VAR") };
78        let resolver = EnvResolver;
79        let source = SecretSource::Custom {
80            provider: "env".into(),
81            config: serde_json::json!({ "var_name": "NEURON_TEST_MISSING_VAR" }),
82        };
83        let err = resolver.resolve(&source).await.unwrap_err();
84        assert!(matches!(err, SecretError::NotFound(_)));
85        assert!(err.to_string().contains("NEURON_TEST_MISSING_VAR"));
86    }
87
88    #[tokio::test]
89    async fn rejects_non_custom_source() {
90        let resolver = EnvResolver;
91        let source = SecretSource::Vault {
92            mount: "secret".into(),
93            path: "data/key".into(),
94        };
95        let err = resolver.resolve(&source).await.unwrap_err();
96        assert!(matches!(err, SecretError::NoResolver(_)));
97    }
98
99    #[tokio::test]
100    async fn rejects_custom_with_wrong_provider() {
101        let resolver = EnvResolver;
102        let source = SecretSource::Custom {
103            provider: "1password".into(),
104            config: serde_json::json!({}),
105        };
106        let err = resolver.resolve(&source).await.unwrap_err();
107        assert!(matches!(err, SecretError::NoResolver(_)));
108    }
109
110    #[tokio::test]
111    async fn rejects_missing_var_name_config() {
112        let resolver = EnvResolver;
113        let source = SecretSource::Custom {
114            provider: "env".into(),
115            config: serde_json::json!({}),
116        };
117        let err = resolver.resolve(&source).await.unwrap_err();
118        assert!(matches!(err, SecretError::NotFound(_)));
119        assert!(err.to_string().contains("var_name"));
120    }
121}