use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use tracing::instrument;
use zeroize::Zeroizing;
use cellos_core::ports::SecretBroker;
use cellos_core::{CellosError, SecretView};
type MaterializedSecrets = HashMap<(String, String), Zeroizing<String>>;
#[derive(Clone, Default)]
pub struct MemorySecretBroker {
inner: Arc<Mutex<MaterializedSecrets>>,
}
impl MemorySecretBroker {
pub fn new() -> Self {
Self::default()
}
pub async fn materialized_keys_for_cell(&self, cell_id: &str) -> Vec<String> {
let g = self.inner.lock().await;
g.keys()
.filter(|(c, _)| c == cell_id)
.map(|(_, k)| k.clone())
.collect()
}
pub async fn total_materialized_rows(&self) -> usize {
self.inner.lock().await.len()
}
}
#[async_trait]
impl SecretBroker for MemorySecretBroker {
#[instrument(skip(self))]
async fn resolve(
&self,
key: &str,
cell_id: &str,
_ttl_seconds: u64,
) -> Result<SecretView, CellosError> {
if key.is_empty() {
return Err(CellosError::SecretBroker("empty secret key".into()));
}
let value = Zeroizing::new(format!("simulated:{cell_id}:{key}"));
let view_value = value.to_string();
self.inner
.lock()
.await
.insert((cell_id.to_string(), key.to_string()), value);
Ok(SecretView {
key: key.to_string(),
value: zeroize::Zeroizing::new(view_value),
})
}
#[instrument(skip(self))]
async fn revoke_for_cell(&self, cell_id: &str) -> Result<(), CellosError> {
let mut g = self.inner.lock().await;
g.retain(|(c, _), _| c != cell_id);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn revoke_drops_all_for_cell() {
let b = MemorySecretBroker::new();
b.resolve("K", "c1", 10).await.unwrap();
b.resolve("K2", "c1", 10).await.unwrap();
b.resolve("K", "c2", 10).await.unwrap();
assert_eq!(b.materialized_keys_for_cell("c1").await.len(), 2);
b.revoke_for_cell("c1").await.unwrap();
assert!(b.materialized_keys_for_cell("c1").await.is_empty());
assert_eq!(b.materialized_keys_for_cell("c2").await.len(), 1);
}
}