alien_bindings/providers/vault/
local.rs1use 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#[derive(Debug)]
12pub struct LocalVault {
13 vault_name: String,
14 vault_dir: PathBuf,
15}
16
17impl LocalVault {
18 pub fn new(vault_name: String, vault_dir: PathBuf) -> Self {
24 Self {
25 vault_name,
26 vault_dir,
27 }
28 }
29
30 fn secrets_file_path(&self) -> PathBuf {
32 self.vault_dir.join("secrets.json")
33 }
34
35 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 async fn save_secrets(&self, secrets: &HashMap<String, String>) -> Result<()> {
67 let secrets_file = self.secrets_file_path();
68
69 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 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 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 async fn delete_secret(&self, secret_name: &str) -> Result<()> {
136 let mut secrets = self.load_secrets().await?;
137
138 secrets.remove(secret_name);
139
140 self.save_secrets(&secrets).await
141 }
142
143 async fn list_secrets(&self) -> Result<Vec<String>> {
146 let secrets = self.load_secrets().await?;
147 Ok(secrets.into_keys().collect())
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::Vault as _;
155 use std::collections::BTreeSet;
156 use tempfile::TempDir;
157
158 fn test_vault() -> (LocalVault, TempDir) {
159 let temp_dir = TempDir::new().expect("tempdir");
160 let vault = LocalVault::new("secrets".to_string(), temp_dir.path().to_path_buf());
161 (vault, temp_dir)
162 }
163
164 #[tokio::test]
165 async fn list_secrets_empty_when_no_file() {
166 let (vault, _temp_dir) = test_vault();
167 let names = vault.list_secrets().await.expect("list should succeed");
168 assert!(names.is_empty(), "fresh vault must list no secrets");
169 }
170
171 #[tokio::test]
172 async fn list_secrets_returns_all_stored_names() {
173 let (vault, _temp_dir) = test_vault();
174
175 vault.set_secret("api-key", "a").await.expect("set api-key");
176 vault.set_secret("db-url", "b").await.expect("set db-url");
177
178 let names: BTreeSet<String> = vault
179 .list_secrets()
180 .await
181 .expect("list should succeed")
182 .into_iter()
183 .collect();
184 assert_eq!(
185 names,
186 BTreeSet::from(["api-key".to_string(), "db-url".to_string()]),
187 "list must return exactly the stored secret names"
188 );
189
190 assert_eq!(vault.get_secret("api-key").await.expect("get"), "a");
192 }
193
194 #[tokio::test]
195 async fn list_secrets_reflects_deletion() {
196 let (vault, _temp_dir) = test_vault();
197
198 vault.set_secret("keep", "1").await.expect("set keep");
199 vault.set_secret("drop", "2").await.expect("set drop");
200 vault.delete_secret("drop").await.expect("delete drop");
201 vault
202 .delete_secret("drop")
203 .await
204 .expect("repeated delete is idempotent");
205
206 let names = vault.list_secrets().await.expect("list should succeed");
207 assert_eq!(
208 names,
209 vec!["keep".to_string()],
210 "deleted secret must be gone"
211 );
212 }
213}