1use async_trait::async_trait;
2
3use super::{
4 emit_secret_access_event, RotationHandle, SecretBytes, SecretError, SecretId, SecretMeta,
5 SecretProvider, SecretVersion,
6};
7
8#[derive(Debug, Clone)]
9pub struct EnvSecretProvider {
10 namespace: String,
11}
12
13impl EnvSecretProvider {
14 pub fn new(namespace: impl Into<String>) -> Self {
15 Self {
16 namespace: namespace.into(),
17 }
18 }
19
20 pub fn env_var_name(&self, id: &SecretId) -> String {
21 let namespace = normalize_env_component(&id.namespace);
22 let name = normalize_env_component(&id.name);
23 match id.version {
24 SecretVersion::Latest => format!("HARN_SECRET_{namespace}_{name}"),
25 SecretVersion::Exact(version) => format!("HARN_SECRET_{namespace}_{name}_V{version}"),
26 }
27 }
28}
29
30#[async_trait]
31impl SecretProvider for EnvSecretProvider {
32 async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
33 let env_name = self.env_var_name(id);
34 match std::env::var(&env_name) {
35 Ok(value) if !value.is_empty() => {
36 emit_secret_access_event("env", id);
37 Ok(SecretBytes::from(value))
38 }
39 _ => Err(SecretError::NotFound {
40 provider: "env".to_string(),
41 id: id.clone(),
42 }),
43 }
44 }
45
46 async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
47 let env_name = self.env_var_name(id);
48 let rendered = value.with_exposed(|bytes| {
49 std::str::from_utf8(bytes)
50 .map(|text| text.to_string())
51 .map_err(|error| SecretError::Backend {
52 provider: "env".to_string(),
53 message: format!("env secrets must be valid UTF-8: {error}"),
54 })
55 })?;
56 std::env::set_var(&env_name, rendered);
57 Ok(())
58 }
59
60 async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
61 Err(SecretError::Unsupported {
62 provider: "env".to_string(),
63 operation: "rotate",
64 })
65 }
66
67 async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
68 let env_prefix = if prefix.name.is_empty() {
69 format!(
70 "HARN_SECRET_{}_",
71 normalize_env_component(&prefix.namespace)
72 )
73 } else {
74 self.env_var_name(prefix)
75 };
76
77 let items = std::env::vars()
78 .filter_map(|(name, _)| {
79 if !name.starts_with(&env_prefix) {
80 return None;
81 }
82 let suffix = name
83 .strip_prefix(&format!(
84 "HARN_SECRET_{}_",
85 normalize_env_component(&prefix.namespace)
86 ))
87 .unwrap_or_default()
88 .trim_start_matches('_')
89 .to_ascii_lowercase();
90 Some(SecretMeta {
91 id: SecretId::new(prefix.namespace.clone(), suffix),
92 provider: "env".to_string(),
93 })
94 })
95 .collect::<Vec<_>>();
96 Ok(items)
97 }
98
99 fn namespace(&self) -> &str {
100 &self.namespace
101 }
102
103 fn supports_versions(&self) -> bool {
104 false
105 }
106}
107
108fn normalize_env_component(value: &str) -> String {
109 let mut normalized = String::with_capacity(value.len());
110 let mut last_was_underscore = false;
111 for ch in value.chars() {
112 let mapped = if ch.is_ascii_alphanumeric() {
113 ch.to_ascii_uppercase()
114 } else {
115 '_'
116 };
117 if mapped == '_' {
118 if !last_was_underscore {
119 normalized.push(mapped);
120 }
121 last_was_underscore = true;
122 } else {
123 normalized.push(mapped);
124 last_was_underscore = false;
125 }
126 }
127
128 normalized.trim_matches('_').to_string()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn env_provider_uses_expected_variable_name() {
137 let provider = EnvSecretProvider::new("harn/test");
138 let id = SecretId::new("harn.orchestrator.github", "installation-12345/private-key");
139 assert_eq!(
140 provider.env_var_name(&id),
141 "HARN_SECRET_HARN_ORCHESTRATOR_GITHUB_INSTALLATION_12345_PRIVATE_KEY"
142 );
143 }
144}