Skip to main content

alien_bindings/providers/vault/
local.rs

1use crate::error::{ErrorData, Result};
2use alien_error::{AlienError, Context, IntoAlienError};
3use async_trait::async_trait;
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7/// Local vault binding implementation for development and testing.
8///
9/// Secrets are stored in a JSON file in the vault directory without encryption.
10/// Encryption will be added in a future iteration.
11#[derive(Debug)]
12pub struct LocalVault {
13    vault_name: String,
14    vault_dir: PathBuf,
15}
16
17impl LocalVault {
18    /// Create a new local vault binding.
19    ///
20    /// # Arguments
21    /// * `vault_name` - Name of the vault
22    /// * `vault_dir` - Directory where secrets are stored
23    pub fn new(vault_name: String, vault_dir: PathBuf) -> Self {
24        Self {
25            vault_name,
26            vault_dir,
27        }
28    }
29
30    /// Get the path to the secrets file.
31    fn secrets_file_path(&self) -> PathBuf {
32        self.vault_dir.join("secrets.json")
33    }
34
35    /// Load secrets from disk.
36    async fn load_secrets(&self) -> Result<HashMap<String, String>> {
37        let secrets_file = self.secrets_file_path();
38
39        if !secrets_file.exists() {
40            return Ok(HashMap::new());
41        }
42
43        let content = tokio::fs::read_to_string(&secrets_file)
44            .await
45            .into_alien_error()
46            .context(ErrorData::CloudPlatformError {
47                message: format!(
48                    "Failed to read vault secrets file: {}",
49                    secrets_file.display()
50                ),
51                resource_id: None,
52            })?;
53
54        serde_json::from_str(&content)
55            .into_alien_error()
56            .context(ErrorData::CloudPlatformError {
57                message: format!(
58                    "Failed to parse vault secrets file: {}",
59                    secrets_file.display()
60                ),
61                resource_id: None,
62            })
63    }
64
65    /// Save secrets to disk.
66    async fn save_secrets(&self, secrets: &HashMap<String, String>) -> Result<()> {
67        let secrets_file = self.secrets_file_path();
68
69        // Ensure directory exists
70        if let Some(parent) = secrets_file.parent() {
71            tokio::fs::create_dir_all(parent)
72                .await
73                .into_alien_error()
74                .context(ErrorData::CloudPlatformError {
75                    message: format!("Failed to create vault directory: {}", parent.display()),
76                    resource_id: None,
77                })?;
78        }
79
80        let json = serde_json::to_string_pretty(secrets)
81            .into_alien_error()
82            .context(ErrorData::CloudPlatformError {
83                message: "Failed to serialize vault secrets".to_string(),
84                resource_id: None,
85            })?;
86
87        let path = secrets_file.clone();
88        let data = json.into_bytes();
89        tokio::task::spawn_blocking(move || alien_core::file_utils::write_secret_file(&path, &data))
90            .await
91            .into_alien_error()
92            .context(ErrorData::CloudPlatformError {
93                message: "Failed to spawn blocking write task".to_string(),
94                resource_id: None,
95            })?
96            .into_alien_error()
97            .context(ErrorData::CloudPlatformError {
98                message: format!(
99                    "Failed to write vault secrets file: {}",
100                    secrets_file.display()
101                ),
102                resource_id: None,
103            })
104    }
105}
106
107#[async_trait]
108impl crate::traits::Binding for LocalVault {}
109
110#[async_trait]
111impl crate::traits::Vault for LocalVault {
112    /// Get a secret value by name
113    async fn get_secret(&self, secret_name: &str) -> Result<String> {
114        let secrets = self.load_secrets().await?;
115
116        secrets.get(secret_name).cloned().ok_or_else(|| {
117            AlienError::new(ErrorData::CloudPlatformError {
118                message: format!(
119                    "Secret '{}' not found in vault '{}'",
120                    secret_name, self.vault_name
121                ),
122                resource_id: None,
123            })
124        })
125    }
126
127    /// Set a secret value
128    async fn set_secret(&self, secret_name: &str, value: &str) -> Result<()> {
129        let mut secrets = self.load_secrets().await?;
130        secrets.insert(secret_name.to_string(), value.to_string());
131        self.save_secrets(&secrets).await
132    }
133
134    /// Delete a secret
135    async fn delete_secret(&self, secret_name: &str) -> Result<()> {
136        let mut secrets = self.load_secrets().await?;
137
138        secrets.remove(secret_name).ok_or_else(|| {
139            AlienError::new(ErrorData::CloudPlatformError {
140                message: format!(
141                    "Secret '{}' not found in vault '{}'",
142                    secret_name, self.vault_name
143                ),
144                resource_id: None,
145            })
146        })?;
147
148        self.save_secrets(&secrets).await
149    }
150}