2025-11-30 16:25:51 -03:00
|
|
|
//! Secrets Management Module
|
|
|
|
|
//!
|
2025-12-03 22:23:30 -03:00
|
|
|
//! Provides integration with HashiCorp Vault for secure secrets management
|
|
|
|
|
//! using the `vaultrs` library.
|
2025-11-30 16:25:51 -03:00
|
|
|
//!
|
|
|
|
|
//! With Vault, .env contains ONLY:
|
|
|
|
|
//! - VAULT_ADDR - Vault server address
|
|
|
|
|
//! - VAULT_TOKEN - Vault authentication token
|
|
|
|
|
//!
|
|
|
|
|
//! Vault paths:
|
2025-12-03 22:23:30 -03:00
|
|
|
//! - gbo/directory - Zitadel connection
|
|
|
|
|
//! - gbo/tables - PostgreSQL credentials
|
|
|
|
|
//! - gbo/drive - MinIO/S3 credentials
|
|
|
|
|
//! - gbo/cache - Redis credentials
|
|
|
|
|
//! - gbo/email - Email credentials
|
|
|
|
|
//! - gbo/llm - LLM API keys
|
|
|
|
|
//! - gbo/encryption - Encryption keys
|
|
|
|
|
//! - gbo/meet - LiveKit credentials
|
|
|
|
|
//! - gbo/alm - Forgejo credentials
|
|
|
|
|
//! - gbo/vectordb - Qdrant credentials
|
|
|
|
|
//! - gbo/observability - InfluxDB credentials
|
|
|
|
|
|
|
|
|
|
use anyhow::{anyhow, Result};
|
|
|
|
|
use log::{debug, info, warn};
|
2025-11-30 16:25:51 -03:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::sync::Arc;
|
2025-12-03 22:23:30 -03:00
|
|
|
use std::sync::Arc as StdArc;
|
2025-11-30 16:25:51 -03:00
|
|
|
use tokio::sync::RwLock;
|
2025-12-03 22:23:30 -03:00
|
|
|
use vaultrs::client::{VaultClient, VaultClientSettingsBuilder};
|
|
|
|
|
use vaultrs::kv2;
|
2025-11-30 16:25:51 -03:00
|
|
|
|
|
|
|
|
/// Secret paths in Vault
|
2025-12-02 21:09:43 -03:00
|
|
|
#[derive(Debug)]
|
2025-11-30 16:25:51 -03:00
|
|
|
pub struct SecretPaths;
|
|
|
|
|
|
|
|
|
|
impl SecretPaths {
|
|
|
|
|
pub const DIRECTORY: &'static str = "gbo/directory";
|
|
|
|
|
pub const TABLES: &'static str = "gbo/tables";
|
|
|
|
|
pub const DRIVE: &'static str = "gbo/drive";
|
|
|
|
|
pub const CACHE: &'static str = "gbo/cache";
|
|
|
|
|
pub const EMAIL: &'static str = "gbo/email";
|
|
|
|
|
pub const LLM: &'static str = "gbo/llm";
|
|
|
|
|
pub const ENCRYPTION: &'static str = "gbo/encryption";
|
|
|
|
|
pub const MEET: &'static str = "gbo/meet";
|
|
|
|
|
pub const ALM: &'static str = "gbo/alm";
|
|
|
|
|
pub const VECTORDB: &'static str = "gbo/vectordb";
|
|
|
|
|
pub const OBSERVABILITY: &'static str = "gbo/observability";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Cached secret with expiry
|
|
|
|
|
struct CachedSecret {
|
|
|
|
|
data: HashMap<String, String>,
|
|
|
|
|
expires_at: std::time::Instant,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
/// Secrets manager using vaultrs
|
2025-11-30 16:25:51 -03:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct SecretsManager {
|
2025-12-03 22:23:30 -03:00
|
|
|
client: Option<StdArc<VaultClient>>,
|
2025-11-30 16:25:51 -03:00
|
|
|
cache: Arc<RwLock<HashMap<String, CachedSecret>>>,
|
2025-12-03 22:23:30 -03:00
|
|
|
cache_ttl: u64,
|
2025-11-30 16:25:51 -03:00
|
|
|
enabled: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 21:09:43 -03:00
|
|
|
impl std::fmt::Debug for SecretsManager {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.debug_struct("SecretsManager")
|
|
|
|
|
.field("enabled", &self.enabled)
|
2025-12-03 22:23:30 -03:00
|
|
|
.field("cache_ttl", &self.cache_ttl)
|
|
|
|
|
.finish()
|
2025-12-02 21:09:43 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 16:25:51 -03:00
|
|
|
impl SecretsManager {
|
2025-12-03 22:23:30 -03:00
|
|
|
/// Create from environment variables
|
|
|
|
|
pub fn from_env() -> Result<Self> {
|
|
|
|
|
let addr = env::var("VAULT_ADDR").unwrap_or_default();
|
|
|
|
|
let token = env::var("VAULT_TOKEN").unwrap_or_default();
|
|
|
|
|
let skip_verify = env::var("VAULT_SKIP_VERIFY")
|
|
|
|
|
.map(|v| v == "true" || v == "1")
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
let cache_ttl = env::var("VAULT_CACHE_TTL")
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|v| v.parse().ok())
|
|
|
|
|
.unwrap_or(300);
|
|
|
|
|
|
|
|
|
|
let enabled = !token.is_empty() && !addr.is_empty();
|
2025-11-30 16:25:51 -03:00
|
|
|
|
|
|
|
|
if !enabled {
|
2025-12-03 22:23:30 -03:00
|
|
|
warn!("Vault not configured. Using environment variables directly.");
|
|
|
|
|
return Ok(Self {
|
|
|
|
|
client: None,
|
|
|
|
|
cache: Arc::new(RwLock::new(HashMap::new())),
|
|
|
|
|
cache_ttl,
|
|
|
|
|
enabled: false,
|
|
|
|
|
});
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
let settings = VaultClientSettingsBuilder::default()
|
|
|
|
|
.address(&addr)
|
|
|
|
|
.token(&token)
|
|
|
|
|
.verify(!skip_verify)
|
|
|
|
|
.build()?;
|
|
|
|
|
|
|
|
|
|
let client = VaultClient::new(settings)?;
|
|
|
|
|
|
|
|
|
|
info!("Vault client initialized: {}", addr);
|
2025-11-30 16:25:51 -03:00
|
|
|
|
|
|
|
|
Ok(Self {
|
2025-12-03 22:23:30 -03:00
|
|
|
client: Some(StdArc::new(client)),
|
2025-11-30 16:25:51 -03:00
|
|
|
cache: Arc::new(RwLock::new(HashMap::new())),
|
2025-12-03 22:23:30 -03:00
|
|
|
cache_ttl,
|
|
|
|
|
enabled: true,
|
2025-11-30 16:25:51 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_enabled(&self) -> bool {
|
|
|
|
|
self.enabled
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
/// Get a secret from Vault or env fallback
|
2025-11-30 16:25:51 -03:00
|
|
|
pub async fn get_secret(&self, path: &str) -> Result<HashMap<String, String>> {
|
|
|
|
|
if !self.enabled {
|
|
|
|
|
return self.get_from_env(path);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
// Check cache
|
2025-11-30 16:25:51 -03:00
|
|
|
if let Some(cached) = self.get_cached(path).await {
|
|
|
|
|
return Ok(cached);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch from Vault
|
2025-12-03 22:23:30 -03:00
|
|
|
let client = self
|
|
|
|
|
.client
|
|
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow!("No Vault client"))?;
|
|
|
|
|
|
|
|
|
|
let result: Result<HashMap<String, String>, _> =
|
|
|
|
|
kv2::read(client.as_ref(), "secret", path).await;
|
|
|
|
|
|
|
|
|
|
let data = match result {
|
|
|
|
|
Ok(d) => d,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
debug!(
|
|
|
|
|
"Vault read failed for '{}': {}, falling back to env",
|
|
|
|
|
path, e
|
|
|
|
|
);
|
|
|
|
|
return self.get_from_env(path);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-11-30 16:25:51 -03:00
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
// Cache result
|
|
|
|
|
if self.cache_ttl > 0 {
|
|
|
|
|
self.cache_secret(path, data.clone()).await;
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
Ok(data)
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_value(&self, path: &str, key: &str) -> Result<String> {
|
2025-12-03 22:23:30 -03:00
|
|
|
self.get_secret(path)
|
|
|
|
|
.await?
|
2025-11-30 16:25:51 -03:00
|
|
|
.get(key)
|
|
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.ok_or_else(|| anyhow!("Key '{}' not found in '{}'", key, path))
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
// Convenience methods for specific secrets
|
|
|
|
|
|
2025-11-30 16:25:51 -03:00
|
|
|
pub async fn get_drive_credentials(&self) -> Result<(String, String)> {
|
2025-12-03 22:23:30 -03:00
|
|
|
let s = self.get_secret(SecretPaths::DRIVE).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
Ok((
|
2025-12-03 22:23:30 -03:00
|
|
|
s.get("accesskey").cloned().unwrap_or_default(),
|
|
|
|
|
s.get("secret").cloned().unwrap_or_default(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn get_database_config(&self) -> Result<(String, u16, String, String, String)> {
|
|
|
|
|
let s = self.get_secret(SecretPaths::TABLES).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
Ok((
|
2025-12-03 22:23:30 -03:00
|
|
|
s.get("host").cloned().unwrap_or_else(|| "localhost".into()),
|
|
|
|
|
s.get("port").and_then(|p| p.parse().ok()).unwrap_or(5432),
|
|
|
|
|
s.get("database")
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_else(|| "botserver".into()),
|
|
|
|
|
s.get("username")
|
2025-11-30 16:25:51 -03:00
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.unwrap_or_else(|| "gbuser".into()),
|
|
|
|
|
s.get("password").cloned().unwrap_or_default(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn get_database_url(&self) -> Result<String> {
|
|
|
|
|
let (host, port, db, user, pass) = self.get_database_config().await?;
|
|
|
|
|
Ok(format!(
|
|
|
|
|
"postgres://{}:{}@{}:{}/{}",
|
|
|
|
|
user, pass, host, port, db
|
|
|
|
|
))
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn get_database_credentials(&self) -> Result<(String, String)> {
|
|
|
|
|
let s = self.get_secret(SecretPaths::TABLES).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
Ok((
|
2025-12-03 22:23:30 -03:00
|
|
|
s.get("username")
|
2025-11-30 16:25:51 -03:00
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.unwrap_or_else(|| "gbuser".into()),
|
|
|
|
|
s.get("password").cloned().unwrap_or_default(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn get_cache_password(&self) -> Result<Option<String>> {
|
|
|
|
|
Ok(self
|
|
|
|
|
.get_secret(SecretPaths::CACHE)
|
|
|
|
|
.await?
|
|
|
|
|
.get("password")
|
|
|
|
|
.cloned())
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn get_directory_config(&self) -> Result<(String, String, String, String)> {
|
|
|
|
|
let s = self.get_secret(SecretPaths::DIRECTORY).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
Ok((
|
2025-12-03 22:23:30 -03:00
|
|
|
s.get("url")
|
2025-11-30 16:25:51 -03:00
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.unwrap_or_else(|| "https://localhost:8080".into()),
|
|
|
|
|
s.get("project_id").cloned().unwrap_or_default(),
|
|
|
|
|
s.get("client_id").cloned().unwrap_or_default(),
|
|
|
|
|
s.get("client_secret").cloned().unwrap_or_default(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn get_directory_credentials(&self) -> Result<(String, String)> {
|
|
|
|
|
let s = self.get_secret(SecretPaths::DIRECTORY).await?;
|
|
|
|
|
Ok((
|
|
|
|
|
s.get("client_id").cloned().unwrap_or_default(),
|
|
|
|
|
s.get("client_secret").cloned().unwrap_or_default(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_vectordb_config(&self) -> Result<(String, Option<String>)> {
|
2025-12-03 22:23:30 -03:00
|
|
|
let s = self.get_secret(SecretPaths::VECTORDB).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
Ok((
|
2025-12-03 22:23:30 -03:00
|
|
|
s.get("url")
|
2025-11-30 16:25:51 -03:00
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.unwrap_or_else(|| "https://localhost:6334".into()),
|
|
|
|
|
s.get("api_key").cloned(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_observability_config(&self) -> Result<(String, String, String, String)> {
|
2025-12-03 22:23:30 -03:00
|
|
|
let s = self.get_secret(SecretPaths::OBSERVABILITY).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
Ok((
|
2025-12-03 22:23:30 -03:00
|
|
|
s.get("url")
|
2025-11-30 16:25:51 -03:00
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.unwrap_or_else(|| "http://localhost:8086".into()),
|
|
|
|
|
s.get("org")
|
2025-11-30 16:25:51 -03:00
|
|
|
.cloned()
|
2025-12-03 22:23:30 -03:00
|
|
|
.unwrap_or_else(|| "pragmatismo".into()),
|
|
|
|
|
s.get("bucket").cloned().unwrap_or_else(|| "metrics".into()),
|
|
|
|
|
s.get("token").cloned().unwrap_or_default(),
|
2025-11-30 16:25:51 -03:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_llm_api_key(&self, provider: &str) -> Result<Option<String>> {
|
2025-12-03 22:23:30 -03:00
|
|
|
let s = self.get_secret(SecretPaths::LLM).await?;
|
|
|
|
|
Ok(s.get(&format!("{}_key", provider.to_lowercase())).cloned())
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_encryption_key(&self) -> Result<String> {
|
2025-12-03 22:23:30 -03:00
|
|
|
self.get_value(SecretPaths::ENCRYPTION, "master_key").await
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn put_secret(&self, path: &str, data: HashMap<String, String>) -> Result<()> {
|
2025-12-03 22:23:30 -03:00
|
|
|
let client = self
|
2025-11-30 16:25:51 -03:00
|
|
|
.client
|
2025-12-03 22:23:30 -03:00
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow!("Vault not enabled"))?;
|
|
|
|
|
kv2::set(client.as_ref(), "secret", path, &data).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
self.invalidate_cache(path).await;
|
|
|
|
|
info!("Secret stored at '{}'", path);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn delete_secret(&self, path: &str) -> Result<()> {
|
2025-12-03 22:23:30 -03:00
|
|
|
let client = self
|
2025-11-30 16:25:51 -03:00
|
|
|
.client
|
2025-12-03 22:23:30 -03:00
|
|
|
.as_ref()
|
|
|
|
|
.ok_or_else(|| anyhow!("Vault not enabled"))?;
|
|
|
|
|
kv2::delete_latest(client.as_ref(), "secret", path).await?;
|
2025-11-30 16:25:51 -03:00
|
|
|
self.invalidate_cache(path).await;
|
|
|
|
|
info!("Secret deleted at '{}'", path);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn health_check(&self) -> Result<bool> {
|
2025-12-03 22:23:30 -03:00
|
|
|
if let Some(client) = &self.client {
|
|
|
|
|
Ok(vaultrs::sys::health(client.as_ref()).await.is_ok())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(false)
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
pub async fn clear_cache(&self) {
|
|
|
|
|
self.cache.write().await.clear();
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_cached(&self, path: &str) -> Option<HashMap<String, String>> {
|
|
|
|
|
let cache = self.cache.read().await;
|
2025-12-03 22:23:30 -03:00
|
|
|
cache
|
|
|
|
|
.get(path)
|
|
|
|
|
.and_then(|c| (c.expires_at > std::time::Instant::now()).then(|| c.data.clone()))
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn cache_secret(&self, path: &str, data: HashMap<String, String>) {
|
2025-12-03 22:23:30 -03:00
|
|
|
self.cache.write().await.insert(
|
2025-11-30 16:25:51 -03:00
|
|
|
path.to_string(),
|
|
|
|
|
CachedSecret {
|
|
|
|
|
data,
|
|
|
|
|
expires_at: std::time::Instant::now()
|
2025-12-03 22:23:30 -03:00
|
|
|
+ std::time::Duration::from_secs(self.cache_ttl),
|
2025-11-30 16:25:51 -03:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn invalidate_cache(&self, path: &str) {
|
2025-12-03 22:23:30 -03:00
|
|
|
self.cache.write().await.remove(path);
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
/// Fallback to environment variables
|
2025-11-30 16:25:51 -03:00
|
|
|
fn get_from_env(&self, path: &str) -> Result<HashMap<String, String>> {
|
|
|
|
|
let mut data = HashMap::new();
|
2025-12-03 22:23:30 -03:00
|
|
|
let env_mappings: &[(&str, &[(&str, &str)])] = &[
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::DRIVE,
|
|
|
|
|
&[("accesskey", "DRIVE_ACCESSKEY"), ("secret", "DRIVE_SECRET")],
|
|
|
|
|
),
|
|
|
|
|
(SecretPaths::CACHE, &[("password", "REDIS_PASSWORD")]),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::DIRECTORY,
|
|
|
|
|
&[
|
|
|
|
|
("url", "DIRECTORY_URL"),
|
|
|
|
|
("project_id", "DIRECTORY_PROJECT_ID"),
|
|
|
|
|
("client_id", "ZITADEL_CLIENT_ID"),
|
|
|
|
|
("client_secret", "ZITADEL_CLIENT_SECRET"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::TABLES,
|
|
|
|
|
&[
|
|
|
|
|
("host", "DB_HOST"),
|
|
|
|
|
("port", "DB_PORT"),
|
|
|
|
|
("database", "DB_NAME"),
|
|
|
|
|
("username", "DB_USER"),
|
|
|
|
|
("password", "DB_PASSWORD"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::VECTORDB,
|
|
|
|
|
&[("url", "QDRANT_URL"), ("api_key", "QDRANT_API_KEY")],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::OBSERVABILITY,
|
|
|
|
|
&[
|
|
|
|
|
("url", "INFLUXDB_URL"),
|
|
|
|
|
("org", "INFLUXDB_ORG"),
|
|
|
|
|
("bucket", "INFLUXDB_BUCKET"),
|
|
|
|
|
("token", "INFLUXDB_TOKEN"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::EMAIL,
|
|
|
|
|
&[("username", "EMAIL_USER"), ("password", "EMAIL_PASSWORD")],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::LLM,
|
|
|
|
|
&[
|
|
|
|
|
("openai_key", "OPENAI_API_KEY"),
|
|
|
|
|
("anthropic_key", "ANTHROPIC_API_KEY"),
|
|
|
|
|
("groq_key", "GROQ_API_KEY"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
(SecretPaths::ENCRYPTION, &[("master_key", "ENCRYPTION_KEY")]),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::MEET,
|
|
|
|
|
&[
|
|
|
|
|
("api_key", "LIVEKIT_API_KEY"),
|
|
|
|
|
("api_secret", "LIVEKIT_API_SECRET"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
SecretPaths::ALM,
|
|
|
|
|
&[
|
|
|
|
|
("url", "ALM_URL"),
|
|
|
|
|
("admin_password", "ALM_ADMIN_PASSWORD"),
|
|
|
|
|
("runner_token", "ALM_RUNNER_TOKEN"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (p, mappings) in env_mappings {
|
|
|
|
|
if *p == path {
|
|
|
|
|
for (key, env_var) in *mappings {
|
|
|
|
|
if let Ok(v) = env::var(env_var) {
|
|
|
|
|
data.insert((*key).to_string(), v);
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 22:23:30 -03:00
|
|
|
break;
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
2025-12-03 22:23:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DATABASE_URL fallback
|
|
|
|
|
if path == SecretPaths::TABLES && data.is_empty() {
|
|
|
|
|
if let Ok(url) = env::var("DATABASE_URL") {
|
|
|
|
|
if let Some(parsed) = parse_database_url(&url) {
|
|
|
|
|
data.extend(parsed);
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(data)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_database_url(url: &str) -> Option<HashMap<String, String>> {
|
|
|
|
|
let url = url.strip_prefix("postgres://")?;
|
|
|
|
|
let (auth, rest) = url.split_once('@')?;
|
|
|
|
|
let (user, pass) = auth.split_once(':').unwrap_or((auth, ""));
|
|
|
|
|
let (host_port, database) = rest.split_once('/').unwrap_or((rest, "botserver"));
|
|
|
|
|
let (host, port) = host_port.split_once(':').unwrap_or((host_port, "5432"));
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
Some(HashMap::from([
|
|
|
|
|
("username".into(), user.into()),
|
|
|
|
|
("password".into(), pass.into()),
|
|
|
|
|
("host".into(), host.into()),
|
|
|
|
|
("port".into(), port.into()),
|
|
|
|
|
("database".into(), database.into()),
|
|
|
|
|
]))
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn init_secrets_manager() -> Result<SecretsManager> {
|
|
|
|
|
SecretsManager::from_env()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct BootstrapConfig {
|
|
|
|
|
pub vault_addr: String,
|
|
|
|
|
pub vault_token: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BootstrapConfig {
|
|
|
|
|
pub fn from_env() -> Result<Self> {
|
|
|
|
|
Ok(Self {
|
2025-12-03 22:23:30 -03:00
|
|
|
vault_addr: env::var("VAULT_ADDR")?,
|
|
|
|
|
vault_token: env::var("VAULT_TOKEN")?,
|
2025-11-30 16:25:51 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_configured() -> bool {
|
|
|
|
|
env::var("VAULT_ADDR").is_ok() && env::var("VAULT_TOKEN").is_ok()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-12-03 22:23:30 -03:00
|
|
|
fn test_parse_database_url() {
|
|
|
|
|
let parsed = parse_database_url("postgres://user:pass@localhost:5432/mydb").unwrap();
|
|
|
|
|
assert_eq!(parsed.get("username"), Some(&"user".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("password"), Some(&"pass".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("host"), Some(&"localhost".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("port"), Some(&"5432".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("database"), Some(&"mydb".to_string()));
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-12-03 22:23:30 -03:00
|
|
|
fn test_parse_database_url_minimal() {
|
|
|
|
|
let parsed = parse_database_url("postgres://user@localhost/mydb").unwrap();
|
|
|
|
|
assert_eq!(parsed.get("username"), Some(&"user".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("password"), Some(&"".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("host"), Some(&"localhost".to_string()));
|
|
|
|
|
assert_eq!(parsed.get("port"), Some(&"5432".to_string()));
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:23:30 -03:00
|
|
|
#[test]
|
|
|
|
|
fn test_secret_paths() {
|
|
|
|
|
assert_eq!(SecretPaths::DIRECTORY, "gbo/directory");
|
|
|
|
|
assert_eq!(SecretPaths::TABLES, "gbo/tables");
|
|
|
|
|
assert_eq!(SecretPaths::LLM, "gbo/llm");
|
2025-11-30 16:25:51 -03:00
|
|
|
}
|
|
|
|
|
}
|