1use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::sync::RwLock;
13
14use async_trait::async_trait;
15use zeroize::Zeroizing;
16
17use super::{SecretProvider, SecretValue};
18use crate::secrets::SecretFileFormat;
19use crate::ContextError;
20
21pub struct FileProvider {
23 base_path: PathBuf,
25 format: SecretFileFormat,
27 cache: RwLock<HashMap<String, HashMap<String, String>>>,
29 allow_writes: bool,
31}
32
33impl FileProvider {
34 pub fn new(path: impl AsRef<Path>, format: SecretFileFormat) -> Result<Self, ContextError> {
41 let base_path = path.as_ref().to_path_buf();
42
43 Ok(Self {
44 base_path,
45 format,
46 cache: RwLock::new(HashMap::new()),
47 allow_writes: false,
48 })
49 }
50
51 pub fn with_writes(mut self) -> Self {
55 self.allow_writes = true;
56 self
57 }
58
59 fn context_file(&self, context_id: &str) -> PathBuf {
61 if self.base_path.is_file() {
62 self.base_path.clone()
63 } else {
64 let ext = self.format.extension();
65 self.base_path.join(format!("{}.{}", context_id, ext))
66 }
67 }
68
69 fn load_file(&self, path: &Path) -> Result<HashMap<String, String>, ContextError> {
71 if !path.exists() {
72 return Ok(HashMap::new());
73 }
74
75 #[cfg(unix)]
77 {
78 use std::os::unix::fs::PermissionsExt;
79 let metadata = fs::metadata(path)?;
80 let mode = metadata.permissions().mode();
81
82 if mode & 0o004 != 0 {
84 tracing::warn!(
85 path = %path.display(),
86 mode = format!("{:o}", mode),
87 "Secrets file is world-readable, consider restricting permissions"
88 );
89 }
90 }
91
92 let content = fs::read_to_string(path)?;
93
94 match self.format {
95 SecretFileFormat::Env => self.parse_env(&content),
96 SecretFileFormat::Json => self.parse_json(&content),
97 SecretFileFormat::Yaml => self.parse_yaml(&content),
98 SecretFileFormat::Raw => {
99 let key = path
101 .file_stem()
102 .and_then(|s| s.to_str())
103 .unwrap_or("secret")
104 .to_string();
105 let mut map = HashMap::new();
106 map.insert(key, content.trim().to_string());
107 Ok(map)
108 }
109 }
110 }
111
112 fn parse_env(&self, content: &str) -> Result<HashMap<String, String>, ContextError> {
114 let mut secrets = HashMap::new();
115
116 for line in content.lines() {
117 let line = line.trim();
118
119 if line.is_empty() || line.starts_with('#') {
121 continue;
122 }
123
124 if let Some((key, value)) = line.split_once('=') {
126 let key = key.trim().to_string();
127 let mut value = value.trim().to_string();
128
129 if (value.starts_with('"') && value.ends_with('"'))
131 || (value.starts_with('\'') && value.ends_with('\''))
132 {
133 value = value[1..value.len() - 1].to_string();
134 }
135
136 secrets.insert(key, value);
137 }
138 }
139
140 Ok(secrets)
141 }
142
143 fn parse_json(&self, content: &str) -> Result<HashMap<String, String>, ContextError> {
145 let value: serde_json::Value = serde_json::from_str(content)?;
146
147 let mut secrets = HashMap::new();
148
149 if let serde_json::Value::Object(map) = value {
150 for (k, v) in map {
151 let string_value = match v {
152 serde_json::Value::String(s) => s,
153 other => other.to_string(),
154 };
155 secrets.insert(k, string_value);
156 }
157 }
158
159 Ok(secrets)
160 }
161
162 fn parse_yaml(&self, content: &str) -> Result<HashMap<String, String>, ContextError> {
164 let value: serde_json::Value = serde_yaml::from_str(content)
167 .map_err(|e| ContextError::Serialization(e.to_string()))?;
168
169 let mut secrets = HashMap::new();
170
171 if let serde_json::Value::Object(map) = value {
172 for (k, v) in map {
173 let string_value = match v {
174 serde_json::Value::String(s) => s,
175 other => other.to_string(),
176 };
177 secrets.insert(k, string_value);
178 }
179 }
180
181 Ok(secrets)
182 }
183
184 fn save_file(&self, path: &Path, secrets: &HashMap<String, String>) -> Result<(), ContextError> {
186 let content = match self.format {
187 SecretFileFormat::Env => {
188 secrets
189 .iter()
190 .map(|(k, v)| format!("{}={}", k, v))
191 .collect::<Vec<_>>()
192 .join("\n")
193 }
194 SecretFileFormat::Json => serde_json::to_string_pretty(secrets)?,
195 SecretFileFormat::Yaml => {
196 serde_yaml::to_string(secrets)
197 .map_err(|e| ContextError::Serialization(e.to_string()))?
198 }
199 SecretFileFormat::Raw => {
200 secrets
202 .values()
203 .next()
204 .cloned()
205 .unwrap_or_default()
206 }
207 };
208
209 if let Some(parent) = path.parent() {
211 fs::create_dir_all(parent)?;
212 }
213
214 fs::write(path, content)?;
215
216 #[cfg(unix)]
218 {
219 use std::os::unix::fs::PermissionsExt;
220 let perms = fs::Permissions::from_mode(0o600);
221 fs::set_permissions(path, perms)?;
222 }
223
224 Ok(())
225 }
226
227 fn get_cached(&self, context_id: &str) -> Result<HashMap<String, String>, ContextError> {
229 {
231 let cache = self.cache.read().unwrap();
232 if let Some(secrets) = cache.get(context_id) {
233 return Ok(secrets.clone());
234 }
235 }
236
237 let file = self.context_file(context_id);
239 let secrets = self.load_file(&file)?;
240
241 {
243 let mut cache = self.cache.write().unwrap();
244 cache.insert(context_id.to_string(), secrets.clone());
245 }
246
247 Ok(secrets)
248 }
249
250 fn invalidate_cache(&self, context_id: &str) {
252 let mut cache = self.cache.write().unwrap();
253 cache.remove(context_id);
254 }
255}
256
257#[async_trait]
258impl SecretProvider for FileProvider {
259 async fn get_secret(
260 &self,
261 context_id: &str,
262 key: &str,
263 ) -> Result<Option<SecretValue>, ContextError> {
264 let secrets = self.get_cached(context_id)?;
265
266 Ok(secrets.get(key).map(|v| Zeroizing::new(v.clone())))
267 }
268
269 async fn set_secret(
270 &self,
271 context_id: &str,
272 key: &str,
273 value: &str,
274 ) -> Result<(), ContextError> {
275 if !self.allow_writes {
276 return Err(ContextError::SecretProvider(
277 "File provider is configured as read-only".to_string(),
278 ));
279 }
280
281 let file = self.context_file(context_id);
282 let mut secrets = self.get_cached(context_id)?;
283
284 secrets.insert(key.to_string(), value.to_string());
285
286 self.save_file(&file, &secrets)?;
287 self.invalidate_cache(context_id);
288
289 tracing::info!(
290 context_id = context_id,
291 key = key,
292 file = %file.display(),
293 "Stored secret in file"
294 );
295
296 Ok(())
297 }
298
299 async fn delete_secret(&self, context_id: &str, key: &str) -> Result<(), ContextError> {
300 if !self.allow_writes {
301 return Err(ContextError::SecretProvider(
302 "File provider is configured as read-only".to_string(),
303 ));
304 }
305
306 let file = self.context_file(context_id);
307 let mut secrets = self.get_cached(context_id)?;
308
309 if secrets.remove(key).is_some() {
310 self.save_file(&file, &secrets)?;
311 self.invalidate_cache(context_id);
312
313 tracing::info!(
314 context_id = context_id,
315 key = key,
316 file = %file.display(),
317 "Deleted secret from file"
318 );
319 }
320
321 Ok(())
322 }
323
324 async fn list_keys(&self, context_id: &str) -> Result<Vec<String>, ContextError> {
325 let secrets = self.get_cached(context_id)?;
326 Ok(secrets.keys().cloned().collect())
327 }
328
329 fn name(&self) -> &'static str {
330 "file"
331 }
332
333 fn is_read_only(&self) -> bool {
334 !self.allow_writes
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use tempfile::TempDir;
342
343 #[tokio::test]
344 async fn test_env_format() {
345 let temp_dir = TempDir::new().unwrap();
346 let secrets_file = temp_dir.path().join("test.env");
347
348 fs::write(
349 &secrets_file,
350 r#"
351# Comment line
352API_KEY=secret123
353DB_PASSWORD="quoted value"
354EMPTY=
355"#,
356 )
357 .unwrap();
358
359 let provider = FileProvider::new(&secrets_file, SecretFileFormat::Env).unwrap();
360
361 let api_key = provider.get_secret("test", "API_KEY").await.unwrap();
362 assert_eq!(&*api_key.unwrap(), "secret123");
363
364 let db_pass = provider.get_secret("test", "DB_PASSWORD").await.unwrap();
365 assert_eq!(&*db_pass.unwrap(), "quoted value");
366
367 let empty = provider.get_secret("test", "EMPTY").await.unwrap();
368 assert_eq!(&*empty.unwrap(), "");
369
370 let missing = provider.get_secret("test", "MISSING").await.unwrap();
371 assert!(missing.is_none());
372 }
373
374 #[tokio::test]
375 async fn test_json_format() {
376 let temp_dir = TempDir::new().unwrap();
377 let secrets_file = temp_dir.path().join("secrets.json");
378
379 fs::write(
380 &secrets_file,
381 r#"{"api_key": "secret123", "number": 42, "nested": {"key": "value"}}"#,
382 )
383 .unwrap();
384
385 let provider = FileProvider::new(&secrets_file, SecretFileFormat::Json).unwrap();
386
387 let api_key = provider.get_secret("secrets", "api_key").await.unwrap();
388 assert_eq!(&*api_key.unwrap(), "secret123");
389
390 let number = provider.get_secret("secrets", "number").await.unwrap();
391 assert_eq!(&*number.unwrap(), "42");
392 }
393
394 #[tokio::test]
395 async fn test_yaml_format() {
396 let temp_dir = TempDir::new().unwrap();
397 let secrets_file = temp_dir.path().join("secrets.yaml");
398
399 fs::write(
400 &secrets_file,
401 r#"
402api_key: secret123
403db_password: "quoted value"
404"#,
405 )
406 .unwrap();
407
408 let provider = FileProvider::new(&secrets_file, SecretFileFormat::Yaml).unwrap();
409
410 let api_key = provider.get_secret("secrets", "api_key").await.unwrap();
411 assert_eq!(&*api_key.unwrap(), "secret123");
412 }
413
414 #[tokio::test]
415 async fn test_directory_mode() {
416 let temp_dir = TempDir::new().unwrap();
417
418 let provider = FileProvider::new(temp_dir.path(), SecretFileFormat::Env).unwrap();
419
420 let ctx_file = temp_dir.path().join("my-context.env");
422 fs::write(&ctx_file, "SECRET_KEY=value123").unwrap();
423
424 let secret = provider.get_secret("my-context", "SECRET_KEY").await.unwrap();
425 assert_eq!(&*secret.unwrap(), "value123");
426 }
427
428 #[tokio::test]
429 async fn test_write_secrets() {
430 let temp_dir = TempDir::new().unwrap();
431 let secrets_file = temp_dir.path().join("writable.env");
432
433 let provider = FileProvider::new(&secrets_file, SecretFileFormat::Env)
434 .unwrap()
435 .with_writes();
436
437 provider.set_secret("writable", "NEW_KEY", "new_value").await.unwrap();
439
440 let secret = provider.get_secret("writable", "NEW_KEY").await.unwrap();
442 assert_eq!(&*secret.unwrap(), "new_value");
443
444 provider.delete_secret("writable", "NEW_KEY").await.unwrap();
446
447 let secret = provider.get_secret("writable", "NEW_KEY").await.unwrap();
449 assert!(secret.is_none());
450 }
451
452 #[tokio::test]
453 async fn test_read_only_mode() {
454 let temp_dir = TempDir::new().unwrap();
455 let secrets_file = temp_dir.path().join("readonly.env");
456 fs::write(&secrets_file, "").unwrap();
457
458 let provider = FileProvider::new(&secrets_file, SecretFileFormat::Env).unwrap();
459
460 let result = provider.set_secret("readonly", "KEY", "value").await;
462 assert!(result.is_err());
463
464 assert!(provider.is_read_only());
465 }
466
467 #[tokio::test]
468 async fn test_list_keys() {
469 let temp_dir = TempDir::new().unwrap();
470 let secrets_file = temp_dir.path().join("list.env");
471
472 fs::write(&secrets_file, "KEY1=v1\nKEY2=v2\nKEY3=v3").unwrap();
473
474 let provider = FileProvider::new(&secrets_file, SecretFileFormat::Env).unwrap();
475
476 let keys = provider.list_keys("list").await.unwrap();
477 assert_eq!(keys.len(), 3);
478 assert!(keys.contains(&"KEY1".to_string()));
479 assert!(keys.contains(&"KEY2".to_string()));
480 assert!(keys.contains(&"KEY3".to_string()));
481 }
482
483 #[tokio::test]
484 async fn test_nonexistent_file() {
485 let temp_dir = TempDir::new().unwrap();
486 let nonexistent = temp_dir.path().join("nonexistent.env");
487
488 let provider = FileProvider::new(&nonexistent, SecretFileFormat::Env).unwrap();
489
490 let result = provider.get_secret("nonexistent", "KEY").await.unwrap();
492 assert!(result.is_none());
493
494 let keys = provider.list_keys("nonexistent").await.unwrap();
496 assert!(keys.is_empty());
497 }
498}