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> {
73 let env_name = self.env_var_name(id);
74 let rendered = value.with_exposed(|bytes| {
75 std::str::from_utf8(bytes)
76 .map(|text| text.to_string())
77 .map_err(|error| SecretError::Backend {
78 provider: "env".to_string(),
79 message: format!("env secrets must be valid UTF-8: {error}"),
80 })
81 })?;
82 unsafe {
87 std::env::set_var(&env_name, rendered);
88 }
89 Ok(())
90 }
91
92 async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
93 Err(SecretError::Unsupported {
94 provider: "env".to_string(),
95 operation: "rotate",
96 })
97 }
98
99 async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
100 let env_prefix = if prefix.name.is_empty() {
101 format!(
102 "HARN_SECRET_{}_",
103 normalize_env_component(&prefix.namespace)
104 )
105 } else {
106 self.env_var_name(prefix)
107 };
108
109 let items = std::env::vars()
110 .filter_map(|(name, _)| {
111 if !name.starts_with(&env_prefix) {
112 return None;
113 }
114 let suffix = name
115 .strip_prefix(&format!(
116 "HARN_SECRET_{}_",
117 normalize_env_component(&prefix.namespace)
118 ))
119 .unwrap_or_default()
120 .trim_start_matches('_')
121 .to_ascii_lowercase();
122 Some(SecretMeta {
123 id: SecretId::new(prefix.namespace.clone(), suffix),
124 provider: "env".to_string(),
125 })
126 })
127 .collect::<Vec<_>>();
128 Ok(items)
129 }
130
131 fn namespace(&self) -> &str {
132 &self.namespace
133 }
134
135 fn supports_versions(&self) -> bool {
136 false
137 }
138}
139
140fn normalize_env_component(value: &str) -> String {
141 let mut normalized = String::with_capacity(value.len());
142 let mut last_was_underscore = false;
143 for ch in value.chars() {
144 let mapped = if ch.is_ascii_alphanumeric() {
145 ch.to_ascii_uppercase()
146 } else {
147 '_'
148 };
149 if mapped == '_' {
150 if !last_was_underscore {
151 normalized.push(mapped);
152 }
153 last_was_underscore = true;
154 } else {
155 normalized.push(mapped);
156 last_was_underscore = false;
157 }
158 }
159
160 normalized.trim_matches('_').to_string()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn env_provider_uses_expected_variable_name() {
169 let provider = EnvSecretProvider::new("harn/test");
170 let id = SecretId::new("harn.orchestrator.github", "installation-12345/private-key");
171 assert_eq!(
172 provider.env_var_name(&id),
173 "HARN_SECRET_HARN_ORCHESTRATOR_GITHUB_INSTALLATION_12345_PRIVATE_KEY"
174 );
175 }
176}