secretumvault/src/storage/surrealdb.rs

377 lines
12 KiB
Rust
Raw Normal View History

2025-12-22 21:34:01 +00:00
//! 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<table_name, HashMap<id, record>>
pub struct SurrealDBBackend {
store: Arc<RwLock<HashMap<String, HashMap<String, Value>>>>,
}
impl SurrealDBBackend {
/// Create a new SurrealDB backend instance
pub async fn new(_config: &SurrealDBStorageConfig) -> StorageResult<Self> {
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<EncryptedData> {
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<Vec<String>> {
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<StoredKey> {
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<Vec<String>> {
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<StoredPolicy> {
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<Vec<String>> {
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<Lease> {
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<Utc>) -> StorageResult<Vec<Lease>> {
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(())
}
}