use chrono::{DateTime, Utc};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use tracing::{debug, error, info, warn};
use crate::integrations::{retry_request, IntegrationError, RetryPolicy};
#[derive(Debug, Clone)]
pub struct VaultConfig {
pub base_url: String,
pub api_key: String,
pub encryption_enabled: bool,
pub compression_enabled: bool,
pub timeout_secs: u64,
pub batch_size: usize,
pub enable_logging: bool,
}
impl VaultConfig {
pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
api_key: api_key.into(),
encryption_enabled: true,
compression_enabled: true,
timeout_secs: 60,
batch_size: 100,
enable_logging: true,
}
}
pub fn with_encryption(mut self, enabled: bool) -> Self {
self.encryption_enabled = enabled;
self
}
pub fn with_compression(mut self, enabled: bool) -> Self {
self.compression_enabled = enabled;
self
}
pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
self.timeout_secs = timeout_secs;
self
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size;
self
}
}
impl Default for VaultConfig {
fn default() -> Self {
Self {
base_url: std::env::var("VAULT_URL")
.unwrap_or_else(|_| "http://localhost:9000".to_string()),
api_key: std::env::var("VAULT_API_KEY")
.expect("VAULT_API_KEY environment variable must be set"),
encryption_enabled: true,
compression_enabled: true,
timeout_secs: 60,
batch_size: 100,
enable_logging: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveEntry {
pub id: String,
pub session_id: String,
pub data: serde_json::Value,
pub archived_at: DateTime<Utc>,
pub retention_days: i64,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
}
impl ArchiveEntry {
pub fn new(
session_id: impl Into<String>,
data: serde_json::Value,
retention_days: i64,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
session_id: session_id.into(),
data,
archived_at: Utc::now(),
retention_days,
tags: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ComplianceLevel {
Standard,
Hipaa,
Gdpr,
Pci,
Soc2,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetentionPolicy {
pub policy_id: String,
pub name: String,
pub retention_days: i64,
pub auto_delete: bool,
pub compliance_level: ComplianceLevel,
#[serde(default)]
pub description: String,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub tags: Vec<String>,
}
impl RetentionPolicy {
pub fn new(
name: impl Into<String>,
retention_days: i64,
compliance_level: ComplianceLevel,
) -> Self {
Self {
policy_id: uuid::Uuid::new_v4().to_string(),
name: name.into(),
retention_days,
auto_delete: false,
compliance_level,
description: String::new(),
created_at: Utc::now(),
tags: Vec::new(),
}
}
pub fn with_auto_delete(mut self, auto_delete: bool) -> Self {
self.auto_delete = auto_delete;
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveResponse {
pub archive_id: String,
pub session_id: String,
pub status: String,
pub archived_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchArchiveResponse {
pub archived: Vec<String>,
#[serde(default)]
pub failed: Vec<ArchiveFailure>,
pub total: usize,
pub success_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveFailure {
pub session_id: String,
pub error: String,
}
pub struct VaultClient {
config: VaultConfig,
client: Client,
retry_policy: RetryPolicy,
}
impl VaultClient {
pub fn new(config: VaultConfig) -> Result<Self, IntegrationError> {
let client = Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.connect_timeout(Duration::from_secs(10))
.pool_idle_timeout(Duration::from_secs(90))
.pool_max_idle_per_host(10)
.build()
.map_err(|e| IntegrationError::HttpError(e.to_string()))?;
let retry_policy = RetryPolicy::new()
.with_max_attempts(3)
.with_initial_delay(Duration::from_millis(200));
Ok(Self {
config,
client,
retry_policy,
})
}
pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = policy;
self
}
pub async fn archive_session(
&self,
entry: ArchiveEntry,
) -> Result<ArchiveResponse, IntegrationError> {
let url = format!("{}/api/v1/archive", self.config.base_url);
if self.config.enable_logging {
info!("Archiving session {} to vault", entry.session_id);
}
let operation = || async {
let mut request = self
.client
.post(&url)
.bearer_auth(&self.config.api_key)
.header(
"X-Encryption-Enabled",
self.config.encryption_enabled.to_string(),
)
.header(
"X-Compression-Enabled",
self.config.compression_enabled.to_string(),
)
.json(&entry);
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
error!("Failed to archive session: {} - {}", status, error_body);
return Err(IntegrationError::ApiError {
status: status.as_u16(),
message: error_body,
});
}
let archive_response: ArchiveResponse = response.json().await?;
Ok(archive_response)
};
let result = retry_request(&self.retry_policy, operation).await?;
if self.config.enable_logging {
info!(
"Session {} archived successfully with ID {}",
entry.session_id, result.archive_id
);
}
Ok(result)
}
pub async fn batch_archive(
&self,
entries: Vec<ArchiveEntry>,
) -> Result<BatchArchiveResponse, IntegrationError> {
let url = format!("{}/api/v1/archive/batch", self.config.base_url);
if self.config.enable_logging {
info!("Batch archiving {} sessions", entries.len());
}
let operation = || async {
let response = self
.client
.post(&url)
.bearer_auth(&self.config.api_key)
.header(
"X-Encryption-Enabled",
self.config.encryption_enabled.to_string(),
)
.header(
"X-Compression-Enabled",
self.config.compression_enabled.to_string(),
)
.json(&entries)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
error!("Failed to batch archive: {} - {}", status, error_body);
return Err(IntegrationError::ApiError {
status: status.as_u16(),
message: error_body,
});
}
let batch_response: BatchArchiveResponse = response.json().await?;
Ok(batch_response)
};
let result = retry_request(&self.retry_policy, operation).await?;
if self.config.enable_logging {
info!(
"Batch archived {} of {} sessions successfully",
result.success_count, result.total
);
}
Ok(result)
}
pub async fn retrieve_session(
&self,
archive_id: &str,
) -> Result<ArchiveEntry, IntegrationError> {
let url = format!("{}/api/v1/archive/{}", self.config.base_url, archive_id);
if self.config.enable_logging {
debug!("Retrieving archive: {}", archive_id);
}
let operation = || async {
let response = self
.client
.get(&url)
.bearer_auth(&self.config.api_key)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
error!("Failed to retrieve archive: {} - {}", status, error_body);
return Err(IntegrationError::ApiError {
status: status.as_u16(),
message: error_body,
});
}
let entry: ArchiveEntry = response.json().await?;
Ok(entry)
};
retry_request(&self.retry_policy, operation).await
}
pub async fn delete_archive(&self, archive_id: &str) -> Result<(), IntegrationError> {
let url = format!("{}/api/v1/archive/{}", self.config.base_url, archive_id);
if self.config.enable_logging {
info!("Deleting archive: {}", archive_id);
}
let operation = || async {
let response = self
.client
.delete(&url)
.bearer_auth(&self.config.api_key)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
error!("Failed to delete archive: {} - {}", status, error_body);
return Err(IntegrationError::ApiError {
status: status.as_u16(),
message: error_body,
});
}
Ok(())
};
retry_request(&self.retry_policy, operation).await
}
pub async fn create_retention_policy(
&self,
policy: RetentionPolicy,
) -> Result<String, IntegrationError> {
let url = format!("{}/api/v1/policies", self.config.base_url);
if self.config.enable_logging {
info!("Creating retention policy: {}", policy.name);
}
let operation = || async {
let response = self
.client
.post(&url)
.bearer_auth(&self.config.api_key)
.json(&policy)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
error!("Failed to create retention policy: {} - {}", status, error_body);
return Err(IntegrationError::ApiError {
status: status.as_u16(),
message: error_body,
});
}
let response_json: serde_json::Value = response.json().await?;
let policy_id = response_json
.get("policy_id")
.and_then(|v| v.as_str())
.ok_or_else(|| IntegrationError::Serialization("Missing policy_id".to_string()))?
.to_string();
Ok(policy_id)
};
retry_request(&self.retry_policy, operation).await
}
pub async fn apply_retention_policy(
&self,
archive_id: &str,
policy_id: &str,
) -> Result<(), IntegrationError> {
let url = format!(
"{}/api/v1/archive/{}/policy",
self.config.base_url, archive_id
);
if self.config.enable_logging {
debug!("Applying policy {} to archive {}", policy_id, archive_id);
}
let operation = || async {
let response = self
.client
.put(&url)
.bearer_auth(&self.config.api_key)
.json(&serde_json::json!({
"policy_id": policy_id
}))
.send()
.await?;
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
error!("Failed to apply retention policy: {} - {}", status, error_body);
return Err(IntegrationError::ApiError {
status: status.as_u16(),
message: error_body,
});
}
Ok(())
};
retry_request(&self.retry_policy, operation).await
}
pub async fn health_check(&self) -> Result<bool, IntegrationError> {
let url = format!("{}/health", self.config.base_url);
let response = self
.client
.get(&url)
.timeout(Duration::from_secs(5))
.send()
.await?;
Ok(response.status().is_success())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vault_config_builder() {
let config = VaultConfig::new("http://localhost:9000", "test-key")
.with_encryption(false)
.with_compression(true)
.with_timeout(120)
.with_batch_size(50);
assert_eq!(config.base_url, "http://localhost:9000");
assert_eq!(config.api_key, "test-key");
assert!(!config.encryption_enabled);
assert!(config.compression_enabled);
assert_eq!(config.timeout_secs, 120);
assert_eq!(config.batch_size, 50);
}
#[test]
fn test_archive_entry_builder() {
let data = serde_json::json!({"key": "value"});
let entry = ArchiveEntry::new("session-123", data, 365)
.with_tag("production")
.with_metadata("user_id", serde_json::json!("user-456"));
assert_eq!(entry.session_id, "session-123");
assert_eq!(entry.retention_days, 365);
assert_eq!(entry.tags.len(), 1);
assert!(entry.metadata.contains_key("user_id"));
}
#[test]
fn test_retention_policy_builder() {
let policy = RetentionPolicy::new("HIPAA Compliance", 2555, ComplianceLevel::Hipaa)
.with_auto_delete(true)
.with_description("7-year retention for HIPAA compliance")
.with_tag("healthcare");
assert_eq!(policy.name, "HIPAA Compliance");
assert_eq!(policy.retention_days, 2555);
assert_eq!(policy.compliance_level, ComplianceLevel::Hipaa);
assert!(policy.auto_delete);
assert_eq!(policy.tags.len(), 1);
}
#[test]
fn test_compliance_level_serialization() {
let level = ComplianceLevel::Gdpr;
let json = serde_json::to_string(&level).unwrap();
assert_eq!(json, "\"gdpr\"");
let level: ComplianceLevel = serde_json::from_str("\"hipaa\"").unwrap();
assert_eq!(level, ComplianceLevel::Hipaa);
}
}