pulseengine_mcp_auth/vault/
mod.rs

1//! Vault integration for centralized secret management
2//!
3//! This module provides integration with external secret management systems
4//! like Infisical, HashiCorp Vault, and others for secure storage and retrieval
5//! of sensitive configuration data.
6
7pub mod infisical;
8
9use async_trait::async_trait;
10use std::collections::HashMap;
11use thiserror::Error;
12
13/// Vault client errors
14#[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/// Secret metadata
36#[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/// Vault client trait for different secret management systems
46#[async_trait]
47pub trait VaultClient: Send + Sync {
48    /// Authenticate with the vault service
49    async fn authenticate(&self) -> Result<(), VaultError>;
50
51    /// Retrieve a secret by name
52    async fn get_secret(&self, name: &str) -> Result<String, VaultError>;
53
54    /// Retrieve a secret with metadata
55    async fn get_secret_with_metadata(
56        &self,
57        name: &str,
58    ) -> Result<(String, SecretMetadata), VaultError>;
59
60    /// List available secrets
61    async fn list_secrets(&self) -> Result<Vec<String>, VaultError>;
62
63    /// Store a secret (if supported)
64    async fn set_secret(&self, name: &str, value: &str) -> Result<(), VaultError>;
65
66    /// Delete a secret (if supported)
67    async fn delete_secret(&self, name: &str) -> Result<(), VaultError>;
68
69    /// Check if the client is authenticated
70    async fn is_authenticated(&self) -> bool;
71
72    /// Get vault client information
73    fn client_info(&self) -> VaultClientInfo;
74}
75
76/// Vault client information
77#[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/// Supported vault types
86#[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/// Vault configuration
110#[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, // 5 minutes
131        }
132    }
133}
134
135/// Create a vault client based on configuration
136pub 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
149/// Vault integration for authentication framework
150pub 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    /// Create a new vault integration
158    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    /// Get a secret with caching
170    pub async fn get_secret_cached(&self, name: &str) -> Result<String, VaultError> {
171        // Check cache first
172        {
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        // Fetch from vault
182        let value = self.client.get_secret(name).await?;
183
184        // Update cache
185        {
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    /// Get master key from vault
194    pub async fn get_master_key(&self) -> Result<String, VaultError> {
195        self.get_secret_cached("PULSEENGINE_MCP_MASTER_KEY").await
196    }
197
198    /// Get API configuration from vault
199    pub async fn get_api_config(&self) -> Result<HashMap<String, String>, VaultError> {
200        let mut config = HashMap::new();
201
202        // Try to get common configuration keys
203        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                    // Optional config, continue
218                }
219                Err(e) => return Err(e),
220            }
221        }
222
223        Ok(config)
224    }
225
226    /// Clear the secret cache
227    pub async fn clear_cache(&self) {
228        let mut cache = self.secret_cache.write().await;
229        cache.clear();
230    }
231
232    /// Get vault client information
233    pub fn client_info(&self) -> VaultClientInfo {
234        self.client.client_info()
235    }
236
237    /// Test vault connectivity
238    pub async fn test_connection(&self) -> Result<(), VaultError> {
239        self.client.authenticate().await?;
240
241        // Try to list secrets to verify access
242        match self.client.list_secrets().await {
243            Ok(_) => Ok(()),
244            Err(VaultError::AccessDenied(_)) => {
245                // Can authenticate but can't list - that's okay
246                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}