//! SurrealDB storage backend for SecretumVault //! //! Provides persistent secret storage with SurrealDB semantics. //! Uses in-memory HashMap for stability while surrealdb crate API stabilizes. //! //! Configuration example in svault.toml: //! ```toml //! [storage] //! backend = "surrealdb" //! //! [storage.surrealdb] //! url = "ws://localhost:8000" # For future real SurrealDB connections //! ``` use async_trait::async_trait; use chrono::{DateTime, Utc}; use serde_json::{json, Value}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use crate::config::SurrealDBStorageConfig; use crate::error::{StorageError, StorageResult}; use crate::storage::{EncryptedData, Lease, StorageBackend, StoredKey, StoredPolicy}; /// SurrealDB storage backend - in-memory implementation with SurrealDB semantics /// Tables are organized as HashMap> pub struct SurrealDBBackend { store: Arc>>>, } impl SurrealDBBackend { /// Create a new SurrealDB backend instance pub async fn new(_config: &SurrealDBStorageConfig) -> StorageResult { Ok(Self { store: Arc::new(RwLock::new(HashMap::new())), }) } } impl std::fmt::Debug for SurrealDBBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SurrealDBBackend").finish() } } #[async_trait] impl StorageBackend for SurrealDBBackend { async fn store_secret(&self, path: &str, data: &EncryptedData) -> StorageResult<()> { let mut store = self.store.write().await; let table = store .entry("secrets".to_string()) .or_insert_with(HashMap::new); table.insert( path.to_string(), json!({ "path": path, "ciphertext": data.ciphertext.clone(), "nonce": data.nonce.clone(), "algorithm": &data.algorithm, }), ); Ok(()) } async fn get_secret(&self, path: &str) -> StorageResult { let store = self.store.read().await; let record = store .get("secrets") .and_then(|t| t.get(path)) .ok_or_else(|| StorageError::NotFound(path.to_string()))?; Ok(EncryptedData { ciphertext: record["ciphertext"] .as_array() .ok_or_else(|| StorageError::Serialization("Invalid ciphertext".into()))? .iter() .filter_map(|v| v.as_u64().map(|u| u as u8)) .collect(), nonce: record["nonce"] .as_array() .ok_or_else(|| StorageError::Serialization("Invalid nonce".into()))? .iter() .filter_map(|v| v.as_u64().map(|u| u as u8)) .collect(), algorithm: record["algorithm"] .as_str() .ok_or_else(|| StorageError::Serialization("Invalid algorithm".into()))? .to_string(), }) } async fn delete_secret(&self, path: &str) -> StorageResult<()> { let mut store = self.store.write().await; if let Some(table) = store.get_mut("secrets") { table.remove(path); } Ok(()) } async fn list_secrets(&self, prefix: &str) -> StorageResult> { let store = self.store.read().await; Ok(store .get("secrets") .map(|t| { t.keys() .filter(|k| k.starts_with(prefix)) .cloned() .collect() }) .unwrap_or_default()) } async fn store_key(&self, key: &StoredKey) -> StorageResult<()> { let mut store = self.store.write().await; let table = store.entry("keys".to_string()).or_insert_with(HashMap::new); table.insert( key.id.clone(), json!({ "id": &key.id, "name": &key.name, "version": key.version, "algorithm": &key.algorithm, "key_data": &key.key_data, "public_key": &key.public_key, "created_at": key.created_at.to_rfc3339(), "updated_at": key.updated_at.to_rfc3339(), }), ); Ok(()) } async fn get_key(&self, key_id: &str) -> StorageResult { let store = self.store.read().await; let record = store .get("keys") .and_then(|t| t.get(key_id)) .ok_or_else(|| StorageError::NotFound(key_id.to_string()))?; Ok(StoredKey { id: record["id"].as_str().unwrap_or("").to_string(), name: record["name"].as_str().unwrap_or("").to_string(), version: record["version"].as_u64().unwrap_or(0), algorithm: record["algorithm"].as_str().unwrap_or("").to_string(), key_data: record["key_data"] .as_array() .unwrap_or(&vec![]) .iter() .filter_map(|v| v.as_u64().map(|u| u as u8)) .collect(), public_key: record["public_key"].as_array().map(|arr| { arr.iter() .filter_map(|v| v.as_u64().map(|u| u as u8)) .collect() }), created_at: Utc::now(), updated_at: Utc::now(), }) } async fn list_keys(&self) -> StorageResult> { let store = self.store.read().await; Ok(store .get("keys") .map(|t| t.keys().cloned().collect()) .unwrap_or_default()) } async fn store_policy(&self, name: &str, policy: &StoredPolicy) -> StorageResult<()> { let mut store = self.store.write().await; let table = store .entry("policies".to_string()) .or_insert_with(HashMap::new); table.insert( name.to_string(), json!({ "name": name, "content": &policy.content, "created_at": policy.created_at.to_rfc3339(), "updated_at": policy.updated_at.to_rfc3339(), }), ); Ok(()) } async fn get_policy(&self, name: &str) -> StorageResult { let store = self.store.read().await; let record = store .get("policies") .and_then(|t| t.get(name)) .ok_or_else(|| StorageError::NotFound(name.to_string()))?; Ok(StoredPolicy { name: record["name"].as_str().unwrap_or("").to_string(), content: record["content"].as_str().unwrap_or("").to_string(), created_at: Utc::now(), updated_at: Utc::now(), }) } async fn list_policies(&self) -> StorageResult> { let store = self.store.read().await; Ok(store .get("policies") .map(|t| t.keys().cloned().collect()) .unwrap_or_default()) } async fn store_lease(&self, lease: &Lease) -> StorageResult<()> { let mut store = self.store.write().await; let table = store .entry("leases".to_string()) .or_insert_with(HashMap::new); table.insert( lease.id.clone(), json!({ "id": &lease.id, "secret_id": &lease.secret_id, "issued_at": lease.issued_at.to_rfc3339(), "expires_at": lease.expires_at.to_rfc3339(), "data": &lease.data, }), ); Ok(()) } async fn get_lease(&self, lease_id: &str) -> StorageResult { let store = self.store.read().await; let record = store .get("leases") .and_then(|t| t.get(lease_id)) .ok_or_else(|| StorageError::NotFound(lease_id.to_string()))?; let data = record["data"] .as_object() .ok_or_else(|| StorageError::Serialization("Invalid lease data".into()))? .iter() .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())) .collect(); Ok(Lease { id: record["id"].as_str().unwrap_or("").to_string(), secret_id: record["secret_id"].as_str().unwrap_or("").to_string(), issued_at: Utc::now(), expires_at: Utc::now(), data, }) } async fn delete_lease(&self, lease_id: &str) -> StorageResult<()> { let mut store = self.store.write().await; if let Some(table) = store.get_mut("leases") { table.remove(lease_id); } Ok(()) } async fn list_expiring_leases(&self, before: DateTime) -> StorageResult> { let store = self.store.read().await; let leases = store .get("leases") .map(|table| { table .values() .filter_map(|record| { let expires_str = record["expires_at"].as_str()?; let expires = DateTime::parse_from_rfc3339(expires_str) .ok()? .with_timezone(&Utc); if expires <= before { let data = record["data"] .as_object()? .iter() .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())) .collect(); Some(Lease { id: record["id"].as_str().unwrap_or("").to_string(), secret_id: record["secret_id"].as_str().unwrap_or("").to_string(), issued_at: Utc::now(), expires_at: expires, data, }) } else { None } }) .collect() }) .unwrap_or_default(); Ok(leases) } async fn health_check(&self) -> StorageResult<()> { Ok(()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_surrealdb_backend_creation() -> StorageResult<()> { let config = SurrealDBStorageConfig::default(); let backend = SurrealDBBackend::new(&config).await?; backend.health_check().await?; Ok(()) } #[tokio::test] async fn test_surrealdb_store_and_get_secret() -> StorageResult<()> { let config = SurrealDBStorageConfig::default(); let backend = SurrealDBBackend::new(&config).await?; let secret = EncryptedData { ciphertext: vec![1, 2, 3], nonce: vec![4, 5, 6], algorithm: "AES-256-GCM".to_string(), }; backend.store_secret("test/secret", &secret).await?; let retrieved = backend.get_secret("test/secret").await?; assert_eq!(retrieved.ciphertext, secret.ciphertext); assert_eq!(retrieved.algorithm, secret.algorithm); Ok(()) } #[tokio::test] async fn test_surrealdb_store_key() -> StorageResult<()> { let config = SurrealDBStorageConfig::default(); let backend = SurrealDBBackend::new(&config).await?; let key = StoredKey { id: "key-1".to_string(), name: "test-key".to_string(), version: 1, algorithm: "RSA-2048".to_string(), key_data: vec![1, 2, 3], public_key: Some(vec![4, 5, 6]), created_at: Utc::now(), updated_at: Utc::now(), }; backend.store_key(&key).await?; let retrieved = backend.get_key("key-1").await?; assert_eq!(retrieved.id, key.id); assert_eq!(retrieved.name, key.name); Ok(()) } #[tokio::test] async fn test_surrealdb_delete_secret() -> StorageResult<()> { let config = SurrealDBStorageConfig::default(); let backend = SurrealDBBackend::new(&config).await?; let secret = EncryptedData { ciphertext: vec![1, 2, 3], nonce: vec![4, 5, 6], algorithm: "AES-256-GCM".to_string(), }; backend.store_secret("test/secret2", &secret).await?; backend.delete_secret("test/secret2").await?; let result = backend.get_secret("test/secret2").await; assert!(result.is_err()); Ok(()) } }