use once_cell::sync::Lazy;
use regex::Regex;
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use super::{ConfigError, ConfigProvider};
const MAX_SECRET_SIZE: u64 = 1024 * 1024;
const MAX_SECRET_NAME_LENGTH: usize = 255;
static SECRET_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"\$\{secret\.([^}]*)\}").expect("Invalid regex pattern"));
#[derive(Debug)]
pub struct VaultConfigProvider {
inner: Arc<dyn ConfigProvider>,
vault_path: PathBuf,
}
impl VaultConfigProvider {
pub fn wrap<P: ConfigProvider + 'static>(provider: P, vault_path: &str) -> Self {
Self {
inner: Arc::new(provider),
vault_path: PathBuf::from(vault_path),
}
}
pub fn wrap_default<P: ConfigProvider + 'static>(provider: P) -> Self {
Self::wrap(provider, "/vault/secret")
}
fn interpolate_secrets(&self, value: Value) -> Result<Value, ConfigError> {
match value {
Value::String(s) => self.interpolate_string(&s).map(Value::String),
Value::Object(mut obj) => {
for (_key, val) in obj.iter_mut() {
*val = self.interpolate_secrets(val.clone())?;
}
Ok(Value::Object(obj))
}
Value::Array(mut arr) => {
for val in arr.iter_mut() {
*val = self.interpolate_secrets(val.clone())?;
}
Ok(Value::Array(arr))
}
other => Ok(other),
}
}
fn interpolate_string(&self, s: &str) -> Result<String, ConfigError> {
let mut result = s.to_string();
let mut secrets_cache: HashMap<String, String> = HashMap::new();
for captures in SECRET_PATTERN.captures_iter(s) {
let full_match = captures.get(0).unwrap().as_str();
let secret_name = captures.get(1).unwrap().as_str();
let secret_value = if let Some(cached_value) = secrets_cache.get(secret_name) {
cached_value.clone()
} else {
let value = self.read_secret(secret_name)?;
secrets_cache.insert(secret_name.to_string(), value.clone());
value
};
result = result.replace(full_match, &secret_value);
}
Ok(result)
}
fn read_secret(&self, name: &str) -> Result<String, ConfigError> {
self.validate_secret_name(name)?;
let secret_path = self.vault_path.join(name);
if !secret_path.exists() {
return Err(ConfigError::provider_error(
"vault",
format!("secret file not found: {}", name),
));
}
let metadata = fs::metadata(&secret_path).map_err(|e| {
ConfigError::provider_error(
"vault",
format!("failed to read secret metadata '{}': {}", name, e),
)
})?;
if metadata.len() > MAX_SECRET_SIZE {
return Err(ConfigError::provider_error(
"vault",
format!(
"secret file too large: {} (max: {} bytes)",
name, MAX_SECRET_SIZE
),
));
}
let content = fs::read_to_string(&secret_path).map_err(|e| {
ConfigError::provider_error("vault", format!("failed to read secret '{}': {}", name, e))
})?;
Ok(content.trim().to_string())
}
fn validate_secret_name(&self, name: &str) -> Result<(), ConfigError> {
if name.is_empty() {
return Err(ConfigError::provider_error("vault", "empty secret name"));
}
if name.len() > MAX_SECRET_NAME_LENGTH {
return Err(ConfigError::provider_error(
"vault",
format!(
"secret name too long: {} (max: {} characters)",
name, MAX_SECRET_NAME_LENGTH
),
));
}
if name.contains("..") || name.contains('/') || name.contains('\\') {
return Err(ConfigError::provider_error(
"vault",
format!("invalid secret name: {}", name),
));
}
Ok(())
}
}
impl ConfigProvider for VaultConfigProvider {
fn get_raw(&self, key: &str) -> Result<Option<Value>, ConfigError> {
match self.inner.get_raw(key)? {
Some(value) => Ok(Some(self.interpolate_secrets(value)?)),
None => Ok(None),
}
}
fn has(&self, key: &str) -> bool {
self.inner.has(key)
}
fn provider_name(&self) -> &str {
"vault"
}
}