use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use chrono::{DateTime, Utc};
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use super::resolver::SecretResolver;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretMetadata {
pub key: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SecretIndex {
pub keys: BTreeMap<String, SecretMetadata>,
}
impl SecretIndex {
pub fn upsert(&mut self, key: &str) {
let now = Utc::now();
self.keys
.entry(key.to_string())
.and_modify(|meta| {
meta.updated_at = now;
})
.or_insert_with(|| SecretMetadata {
key: key.to_string(),
created_at: now,
updated_at: now,
});
}
pub fn remove(&mut self, key: &str) {
self.keys.remove(key);
}
pub fn list(&self) -> Vec<&SecretMetadata> {
self.keys.values().collect()
}
pub fn get(&self, key: &str) -> Option<&SecretMetadata> {
self.keys.get(key)
}
}
pub trait SecretStore {
fn set_secret(&self, key: &str, secret: SecretString) -> Result<()>;
fn get_secret(&self, key: &str) -> Result<Option<SecretString>>;
fn delete_secret(&self, key: &str) -> Result<bool>;
}
#[derive(Debug, Clone, Default)]
pub struct InMemorySecretStore {
values: Arc<Mutex<BTreeMap<String, String>>>,
}
impl SecretStore for InMemorySecretStore {
fn set_secret(&self, key: &str, secret: SecretString) -> Result<()> {
let mut values = self
.values
.lock()
.map_err(|_| anyhow!("in-memory secret store mutex poisoned"))?;
values.insert(key.to_string(), secret.expose_secret().to_string());
Ok(())
}
fn get_secret(&self, key: &str) -> Result<Option<SecretString>> {
let values = self
.values
.lock()
.map_err(|_| anyhow!("in-memory secret store mutex poisoned"))?;
Ok(values.get(key).map(|v| SecretString::new(v.clone().into())))
}
fn delete_secret(&self, key: &str) -> Result<bool> {
let mut values = self
.values
.lock()
.map_err(|_| anyhow!("in-memory secret store mutex poisoned"))?;
Ok(values.remove(key).is_some())
}
}
fn is_transient(err: &anyhow::Error) -> bool {
let msg = format!("{:#}", err).to_lowercase();
msg.contains("timeout")
|| msg.contains("timed out")
|| msg.contains("connection reset")
|| msg.contains("connection refused")
|| msg.contains("broken pipe")
|| msg.contains("http 500")
|| msg.contains("http 502")
|| msg.contains("http 503")
|| msg.contains("http 504")
|| msg.contains("http 429")
|| msg.contains("status code 500")
|| msg.contains("status code 502")
|| msg.contains("status code 503")
|| msg.contains("status code 504")
|| msg.contains("status code 429")
}
pub fn require_secret(
store: &dyn SecretStore,
resolvers: &[Box<dyn SecretResolver>],
key: &str,
) -> Result<String> {
if let Some((scheme, _)) = key.split_once("://") {
for resolver in resolvers {
if resolver.scheme() == scheme {
let max_attempts: u32 = 3;
let backoff_base = Duration::from_millis(500);
for attempt in 1..=max_attempts {
match resolver.resolve(key) {
Ok(secret) => return Ok(secret.expose_secret().to_string()),
Err(err) if attempt < max_attempts && is_transient(&err) => {
std::thread::sleep(backoff_base * attempt);
continue;
}
Err(err) => return Err(err),
}
}
unreachable!("retry loop should have returned");
}
}
bail!("no secret resolver registered for scheme `{scheme}://`");
}
let secret = store
.get_secret(key)?
.ok_or_else(|| anyhow!("missing required secret `{key}`"))?;
Ok(secret.expose_secret().to_string())
}