pulseengine_mcp_auth/vault/
mod.rs1pub mod infisical;
8
9use async_trait::async_trait;
10use std::collections::HashMap;
11use thiserror::Error;
12
13#[derive(Debug, Error)]
15pub enum VaultError {
16 #[error("Authentication failed: {0}")]
17 AuthenticationFailed(String),
18
19 #[error("Secret not found: {0}")]
20 SecretNotFound(String),
21
22 #[error("Network error: {0}")]
23 NetworkError(String),
24
25 #[error("Configuration error: {0}")]
26 ConfigError(String),
27
28 #[error("Access denied: {0}")]
29 AccessDenied(String),
30
31 #[error("Invalid response: {0}")]
32 InvalidResponse(String),
33}
34
35#[derive(Debug, Clone)]
37pub struct SecretMetadata {
38 pub name: String,
39 pub version: Option<String>,
40 pub created_at: Option<chrono::DateTime<chrono::Utc>>,
41 pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
42 pub tags: HashMap<String, String>,
43}
44
45#[async_trait]
47pub trait VaultClient: Send + Sync {
48 async fn authenticate(&self) -> Result<(), VaultError>;
50
51 async fn get_secret(&self, name: &str) -> Result<String, VaultError>;
53
54 async fn get_secret_with_metadata(
56 &self,
57 name: &str,
58 ) -> Result<(String, SecretMetadata), VaultError>;
59
60 async fn list_secrets(&self) -> Result<Vec<String>, VaultError>;
62
63 async fn set_secret(&self, name: &str, value: &str) -> Result<(), VaultError>;
65
66 async fn delete_secret(&self, name: &str) -> Result<(), VaultError>;
68
69 async fn is_authenticated(&self) -> bool;
71
72 fn client_info(&self) -> VaultClientInfo;
74}
75
76#[derive(Debug, Clone)]
78pub struct VaultClientInfo {
79 pub name: String,
80 pub version: String,
81 pub vault_type: VaultType,
82 pub read_only: bool,
83}
84
85#[derive(Debug, Clone, PartialEq)]
87pub enum VaultType {
88 Infisical,
89 HashiCorpVault,
90 AWSSecretsManager,
91 Azure,
92 GoogleSecretManager,
93 Custom(String),
94}
95
96impl std::fmt::Display for VaultType {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 match self {
99 VaultType::Infisical => write!(f, "Infisical"),
100 VaultType::HashiCorpVault => write!(f, "HashiCorp Vault"),
101 VaultType::AWSSecretsManager => write!(f, "AWS Secrets Manager"),
102 VaultType::Azure => write!(f, "Azure Key Vault"),
103 VaultType::GoogleSecretManager => write!(f, "Google Secret Manager"),
104 VaultType::Custom(name) => write!(f, "Custom: {}", name),
105 }
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct VaultConfig {
112 pub vault_type: VaultType,
113 pub base_url: Option<String>,
114 pub environment: Option<String>,
115 pub project_id: Option<String>,
116 pub timeout_seconds: u64,
117 pub retry_attempts: u32,
118 pub cache_ttl_seconds: u64,
119}
120
121impl Default for VaultConfig {
122 fn default() -> Self {
123 Self {
124 vault_type: VaultType::Infisical,
125 base_url: Some("https://app.infisical.com".to_string()),
126 environment: Some("dev".to_string()),
127 project_id: None,
128 timeout_seconds: 30,
129 retry_attempts: 3,
130 cache_ttl_seconds: 300, }
132 }
133}
134
135pub async fn create_vault_client(config: VaultConfig) -> Result<Box<dyn VaultClient>, VaultError> {
137 match config.vault_type {
138 VaultType::Infisical => {
139 let client = infisical::InfisicalClient::new(config).await?;
140 Ok(Box::new(client))
141 }
142 _ => Err(VaultError::ConfigError(format!(
143 "Vault type {} not yet implemented",
144 config.vault_type
145 ))),
146 }
147}
148
149pub struct VaultIntegration {
151 client: Box<dyn VaultClient>,
152 secret_cache: tokio::sync::RwLock<HashMap<String, (String, std::time::Instant)>>,
153 cache_ttl: std::time::Duration,
154}
155
156impl VaultIntegration {
157 pub async fn new(config: VaultConfig) -> Result<Self, VaultError> {
159 let cache_ttl = std::time::Duration::from_secs(config.cache_ttl_seconds);
160 let client = create_vault_client(config).await?;
161
162 Ok(Self {
163 client,
164 secret_cache: tokio::sync::RwLock::new(HashMap::new()),
165 cache_ttl,
166 })
167 }
168
169 pub async fn get_secret_cached(&self, name: &str) -> Result<String, VaultError> {
171 {
173 let cache = self.secret_cache.read().await;
174 if let Some((value, timestamp)) = cache.get(name) {
175 if timestamp.elapsed() < self.cache_ttl {
176 return Ok(value.clone());
177 }
178 }
179 }
180
181 let value = self.client.get_secret(name).await?;
183
184 {
186 let mut cache = self.secret_cache.write().await;
187 cache.insert(name.to_string(), (value.clone(), std::time::Instant::now()));
188 }
189
190 Ok(value)
191 }
192
193 pub async fn get_master_key(&self) -> Result<String, VaultError> {
195 self.get_secret_cached("PULSEENGINE_MCP_MASTER_KEY").await
196 }
197
198 pub async fn get_api_config(&self) -> Result<HashMap<String, String>, VaultError> {
200 let mut config = HashMap::new();
201
202 let config_keys = vec![
204 "PULSEENGINE_MCP_SESSION_TIMEOUT",
205 "PULSEENGINE_MCP_MAX_FAILED_ATTEMPTS",
206 "PULSEENGINE_MCP_RATE_LIMIT_WINDOW",
207 "PULSEENGINE_MCP_ENABLE_AUDIT_LOGGING",
208 "PULSEENGINE_MCP_STORAGE_PATH",
209 ];
210
211 for key in config_keys {
212 match self.get_secret_cached(key).await {
213 Ok(value) => {
214 config.insert(key.to_string(), value);
215 }
216 Err(VaultError::SecretNotFound(_)) => {
217 }
219 Err(e) => return Err(e),
220 }
221 }
222
223 Ok(config)
224 }
225
226 pub async fn clear_cache(&self) {
228 let mut cache = self.secret_cache.write().await;
229 cache.clear();
230 }
231
232 pub fn client_info(&self) -> VaultClientInfo {
234 self.client.client_info()
235 }
236
237 pub async fn test_connection(&self) -> Result<(), VaultError> {
239 self.client.authenticate().await?;
240
241 match self.client.list_secrets().await {
243 Ok(_) => Ok(()),
244 Err(VaultError::AccessDenied(_)) => {
245 Ok(())
247 }
248 Err(e) => Err(e),
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_vault_config_default() {
259 let config = VaultConfig::default();
260 assert_eq!(config.vault_type, VaultType::Infisical);
261 assert_eq!(
262 config.base_url,
263 Some("https://app.infisical.com".to_string())
264 );
265 assert_eq!(config.timeout_seconds, 30);
266 }
267
268 #[test]
269 fn test_vault_type_display() {
270 assert_eq!(VaultType::Infisical.to_string(), "Infisical");
271 assert_eq!(VaultType::HashiCorpVault.to_string(), "HashiCorp Vault");
272 assert_eq!(
273 VaultType::Custom("Test".to_string()).to_string(),
274 "Custom: Test"
275 );
276 }
277}