//! The two store operations the callout's keys need, so a test can stand in //! for the secret store. Every key is kept create-only: a stored seed is read, //! never replaced. use std::future::Future; use anyhow::{Context, Result}; use swarm_secret_client::SecretStore; /// Read and create-only write of one value per path. pub trait KeyStore { /// The value at `path`, or `None` when nothing is stored there. fn read(&self, path: &str) -> impl Future>> + Send; /// Store `value` at `path`, failing when anything is already there. fn create(&self, path: &str, value: &str) -> impl Future> + Send; } /// One field, `value`, as every single-value secret in the store is kept. /// No `Debug`: the values held here include seeds. #[derive(serde::Serialize, serde::Deserialize)] struct Stored { value: String, } impl KeyStore for SecretStore { async fn read(&self, path: &str) -> Result> { let stored: Option = self .read_optional(path) .await .with_context(|| format!("reading {path}"))?; Ok(stored.map(|s| s.value)) } async fn create(&self, path: &str, value: &str) -> Result<()> { SecretStore::create( self, path, &Stored { value: value.to_owned(), }, ) .await .with_context(|| format!("creating {path}")) } } #[cfg(test)] pub(crate) mod tests { use std::collections::HashMap; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; use super::*; /// A store that keeps values in memory and counts writes. `rival` is a /// value another start lands at a path between this one's read and its /// create, which the create then loses to. #[derive(Default)] pub(crate) struct Fake { values: Mutex>, rival: Mutex>, writes: AtomicUsize, } impl Fake { pub(crate) fn holding(entries: &[(&str, &str)]) -> Self { let fake = Self::default(); for (path, value) in entries { fake.put(path, value); } fake } pub(crate) fn put(&self, path: &str, value: &str) { self.values .lock() .expect("unpoisoned") .insert(path.to_owned(), value.to_owned()); } pub(crate) fn get(&self, path: &str) -> Option { self.values.lock().expect("unpoisoned").get(path).cloned() } pub(crate) fn writes(&self) -> usize { self.writes.load(Ordering::SeqCst) } pub(crate) fn rival(&self, path: &str, value: &str) { self.rival .lock() .expect("unpoisoned") .insert(path.to_owned(), value.to_owned()); } } impl KeyStore for Fake { async fn read(&self, path: &str) -> Result> { Ok(self.get(path)) } async fn create(&self, path: &str, value: &str) -> Result<()> { if let Some(theirs) = self.rival.lock().expect("unpoisoned").remove(path) { self.put(path, &theirs); } if self.get(path).is_some() { anyhow::bail!("403 permission denied"); } self.writes.fetch_add(1, Ordering::SeqCst); self.put(path, value); Ok(()) } } }