refactor(secrets): add tenant/org hierarchy with fallback support

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-15 11:38:50 -03:00
parent 7fb73e683f
commit eb586cf4f9

View file

@ -13,18 +13,42 @@ use vaultrs::kv2;
pub struct SecretPaths; pub struct SecretPaths;
impl SecretPaths { impl SecretPaths {
pub const DIRECTORY: &'static str = "gbo/directory"; // System-wide paths (global fallback)
pub const TABLES: &'static str = "gbo/tables"; pub const DIRECTORY: &'static str = "gbo/system/directory";
pub const DRIVE: &'static str = "gbo/drive"; pub const TABLES: &'static str = "gbo/system/tables";
pub const CACHE: &'static str = "gbo/cache"; pub const DRIVE: &'static str = "gbo/system/drive";
pub const EMAIL: &'static str = "gbo/email"; pub const CACHE: &'static str = "gbo/system/cache";
pub const LLM: &'static str = "gbo/llm"; pub const EMAIL: &'static str = "gbo/system/email";
pub const ENCRYPTION: &'static str = "gbo/encryption"; pub const LLM: &'static str = "gbo/system/llm";
pub const JWT: &'static str = "gbo/jwt"; pub const ENCRYPTION: &'static str = "gbo/system/encryption";
pub const MEET: &'static str = "gbo/meet"; pub const JWT: &'static str = "gbo/system/jwt";
pub const ALM: &'static str = "gbo/alm"; pub const MEET: &'static str = "gbo/system/meet";
pub const VECTORDB: &'static str = "gbo/vectordb"; pub const ALM: &'static str = "gbo/system/alm";
pub const OBSERVABILITY: &'static str = "gbo/observability"; pub const VECTORDB: &'static str = "gbo/system/vectordb";
pub const OBSERVABILITY: &'static str = "gbo/system/observability";
pub const SECURITY: &'static str = "gbo/system/security";
pub const CLOUD: &'static str = "gbo/system/cloud";
pub const APP: &'static str = "gbo/system/app";
pub const MODELS: &'static str = "gbo/system/models";
// Tenant infrastructure (per-cluster)
pub fn tenant_infrastructure(tenant: &str) -> String {
format!("gbo/tenants/{}/infrastructure", tenant)
}
pub fn tenant_config(tenant: &str) -> String {
format!("gbo/tenants/{}/config", tenant)
}
// Organization (per-customer)
pub fn org_bot(org_id: &str, bot_id: &str) -> String {
format!("gbo/orgs/{}/bots/{}", org_id, bot_id)
}
pub fn org_user(org_id: &str, user_id: &str) -> String {
format!("gbo/orgs/{}/users/{}", org_id, user_id)
}
pub fn org_config(org_id: &str) -> String {
format!("gbo/orgs/{}/config", org_id)
}
} }
struct CachedSecret { struct CachedSecret {
@ -334,61 +358,93 @@ impl SecretsManager {
fn get_from_env(path: &str) -> Result<HashMap<String, String>> { fn get_from_env(path: &str) -> Result<HashMap<String, String>> {
let mut secrets = HashMap::new(); let mut secrets = HashMap::new();
match path { // Normalize path to handle both old and new formats
SecretPaths::TABLES => { let normalized = if path.starts_with("gbo/system/") {
path.strip_prefix("gbo/system/").unwrap_or(path)
} else {
path
};
match normalized {
"tables" | "gbo/tables" | "system/tables" => {
secrets.insert("host".into(), "localhost".into()); secrets.insert("host".into(), "localhost".into());
secrets.insert("port".into(), "5432".into()); secrets.insert("port".into(), "5432".into());
secrets.insert("database".into(), "botserver".into()); secrets.insert("database".into(), "botserver".into());
secrets.insert("username".into(), "gbuser".into()); secrets.insert("username".into(), "gbuser".into());
secrets.insert("password".into(), "changeme".into()); secrets.insert("password".into(), "changeme".into());
} }
SecretPaths::DIRECTORY => { "directory" | "gbo/directory" | "system/directory" => {
secrets.insert("url".into(), "http://localhost:9000".into()); secrets.insert("url".into(), "http://localhost:9000".into());
secrets.insert("project_id".into(), String::new()); secrets.insert("project_id".into(), String::new());
secrets.insert("client_id".into(), String::new()); secrets.insert("client_id".into(), String::new());
secrets.insert("client_secret".into(), String::new()); secrets.insert("client_secret".into(), String::new());
} }
SecretPaths::DRIVE => { "drive" | "gbo/drive" | "system/drive" => {
secrets.insert("accesskey".into(), String::new()); secrets.insert("host".into(), "localhost".into());
secrets.insert("secret".into(), String::new()); secrets.insert("port".into(), "9000".into());
secrets.insert("accesskey".into(), "minioadmin".into());
secrets.insert("secret".into(), "minioadmin".into());
} }
SecretPaths::CACHE => { "cache" | "gbo/cache" | "system/cache" => {
secrets.insert("host".into(), "localhost".into());
secrets.insert("port".into(), "6379".into());
secrets.insert("password".into(), String::new()); secrets.insert("password".into(), String::new());
} }
SecretPaths::EMAIL => { "email" | "gbo/email" | "system/email" => {
secrets.insert("smtp_host".into(), String::new()); secrets.insert("smtp_host".into(), String::new());
secrets.insert("smtp_port".into(), "587".into()); secrets.insert("smtp_port".into(), "587".into());
secrets.insert("username".into(), String::new()); secrets.insert("smtp_user".into(), String::new());
secrets.insert("password".into(), String::new()); secrets.insert("smtp_password".into(), String::new());
secrets.insert("from_address".into(), String::new()); secrets.insert("smtp_from".into(), String::new());
} }
SecretPaths::LLM => { "llm" | "gbo/llm" | "system/llm" => {
secrets.insert("url".into(), "http://localhost:8081".into());
secrets.insert("model".into(), "gpt-4".into());
secrets.insert("openai_key".into(), String::new()); secrets.insert("openai_key".into(), String::new());
secrets.insert("anthropic_key".into(), String::new()); secrets.insert("anthropic_key".into(), String::new());
secrets.insert("ollama_url".into(), "http://localhost:11434".into()); secrets.insert("ollama_url".into(), "http://localhost:11434".into());
} }
SecretPaths::ENCRYPTION => { "encryption" | "gbo/encryption" | "system/encryption" => {
secrets.insert("master_key".into(), String::new()); secrets.insert("master_key".into(), String::new());
} }
SecretPaths::MEET => { "meet" | "gbo/meet" | "system/meet" => {
secrets.insert("jitsi_url".into(), "https://meet.jit.si".into()); secrets.insert("url".into(), "http://localhost:7880".into());
secrets.insert("app_id".into(), String::new()); secrets.insert("app_id".into(), String::new());
secrets.insert("app_secret".into(), String::new()); secrets.insert("app_secret".into(), String::new());
} }
SecretPaths::VECTORDB => { "vectordb" | "gbo/vectordb" | "system/vectordb" => {
secrets.insert("url".into(), "http://localhost:6333".into()); secrets.insert("url".into(), "http://localhost:6333".into());
secrets.insert("api_key".into(), String::new()); secrets.insert("api_key".into(), String::new());
} }
SecretPaths::OBSERVABILITY => { "observability" | "gbo/observability" | "system/observability" => {
secrets.insert("url".into(), "http://localhost:8086".into()); secrets.insert("url".into(), "http://localhost:8086".into());
secrets.insert("org".into(), "system".into()); secrets.insert("org".into(), "system".into());
secrets.insert("bucket".into(), "metrics".into()); secrets.insert("bucket".into(), "metrics".into());
secrets.insert("token".into(), String::new()); secrets.insert("token".into(), String::new());
} }
SecretPaths::ALM => { "alm" | "gbo/alm" | "system/alm" => {
secrets.insert("url".into(), "http://localhost:9000".into()); secrets.insert("url".into(), "http://localhost:9000".into());
secrets.insert("username".into(), String::new()); secrets.insert("token".into(), String::new());
secrets.insert("password".into(), String::new()); secrets.insert("default_org".into(), String::new());
}
"security" | "gbo/security" | "system/security" => {
secrets.insert("require_auth".into(), "true".into());
secrets.insert("anonymous_paths".into(), String::new());
}
"cloud" | "gbo/cloud" | "system/cloud" => {
secrets.insert("region".into(), "us-east-1".into());
secrets.insert("access_key".into(), String::new());
secrets.insert("secret_key".into(), String::new());
}
"app" | "gbo/app" | "system/app" => {
secrets.insert("url".into(), "http://localhost:8080".into());
secrets.insert("environment".into(), "development".into());
}
"jwt" | "gbo/jwt" | "system/jwt" => {
secrets.insert("secret".into(), String::new());
}
"models" | "gbo/models" | "system/models" => {
secrets.insert("url".into(), "http://localhost:8001".into());
} }
_ => { _ => {
log::debug!("No default values for secret path: {}", path); log::debug!("No default values for secret path: {}", path);
@ -397,6 +453,110 @@ impl SecretsManager {
Ok(secrets) Ok(secrets)
} }
// ============ TENANT INFRASTRUCTURE ============
/// Get database config with tenant fallback to system
pub async fn get_database_config_for_tenant(&self, tenant: &str) -> Result<(String, u16, String, String, String)> {
// Try tenant first
let tenant_path = SecretPaths::tenant_infrastructure(tenant);
if let Ok(s) = self.get_secret(&format!("{}/tables", tenant_path)).await {
return Ok((
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").cloned().unwrap_or_else(|| "gbuser".into()),
s.get("password").cloned().unwrap_or_default(),
));
}
// Fallback to system
self.get_database_config().await
}
/// Get drive config with tenant fallback to system
pub async fn get_drive_config_for_tenant(&self, tenant: &str) -> Result<(String, String, String, String)> {
let tenant_path = SecretPaths::tenant_infrastructure(tenant);
if let Ok(s) = self.get_secret(&format!("{}/drive", tenant_path)).await {
return Ok((
s.get("host").cloned().unwrap_or_else(|| "localhost".into()),
s.get("port").cloned().unwrap_or_else(|| "9000".into()),
s.get("accesskey").cloned().unwrap_or_default(),
s.get("secret").cloned().unwrap_or_default(),
));
}
self.get_drive_credentials().await
}
/// Get cache config with tenant fallback to system
pub async fn get_cache_config_for_tenant(&self, tenant: &str) -> Result<(String, u16, Option<String>)> {
let tenant_path = SecretPaths::tenant_infrastructure(tenant);
if let Ok(s) = self.get_secret(&format!("{}/cache", tenant_path)).await {
return Ok((
s.get("host").cloned().unwrap_or_else(|| "localhost".into()),
s.get("port").and_then(|p| p.parse().ok()).unwrap_or(6379),
s.get("password").cloned(),
));
}
let url = self.get_secret(SecretPaths::CACHE).await?
.get("url").cloned();
let host = url.as_ref().map(|u| u.split("://").nth(1).unwrap_or("localhost").split(':').next().unwrap_or("localhost")).unwrap_or("localhost").to_string();
let port = url.as_ref().and_then(|u| u.split(':').nth(1)).and_then(|p| p.parse().ok()).unwrap_or(6379);
Ok((host, port, None))
}
/// Get SMTP config with tenant fallback to system
pub async fn get_smtp_config_for_tenant(&self, tenant: &str) -> Result<HashMap<String, String>> {
let tenant_path = SecretPaths::tenant_infrastructure(tenant);
if let Ok(s) = self.get_secret(&format!("{}/email", tenant_path)).await {
return Ok(s);
}
self.get_secret(SecretPaths::EMAIL).await
}
/// Get LLM config with tenant fallback to system
pub async fn get_llm_config_for_tenant(&self, tenant: &str) -> Result<HashMap<String, String>> {
let tenant_path = SecretPaths::tenant_infrastructure(tenant);
if let Ok(s) = self.get_secret(&format!("{}/llm", tenant_path)).await {
return Ok(s);
}
self.get_secret(SecretPaths::LLM).await
}
// ============ ORG BOT/USER SECRETS ============
/// Get bot email credentials
pub async fn get_bot_email_config(&self, org_id: &str, bot_id: &str) -> Result<HashMap<String, String>> {
let path = SecretPaths::org_bot(org_id, bot_id);
if let Ok(s) = self.get_secret(&format!("{}/email", path)).await {
return Ok(s);
}
// Fallback to system email
self.get_secret(SecretPaths::EMAIL).await
}
/// Get bot WhatsApp credentials
pub async fn get_bot_whatsapp_config(&self, org_id: &str, bot_id: &str) -> Result<Option<HashMap<String, String>>> {
let path = SecretPaths::org_bot(org_id, bot_id);
Ok(self.get_secret(&format!("{}/whatsapp", path)).await.ok())
}
/// Get bot LLM config (overrides tenant/system)
pub async fn get_bot_llm_config(&self, org_id: &str, bot_id: &str) -> Result<Option<HashMap<String, String>>> {
let path = SecretPaths::org_bot(org_id, bot_id);
Ok(self.get_secret(&format!("{}/llm", path)).await.ok())
}
/// Get user email credentials
pub async fn get_user_email_config(&self, org_id: &str, user_id: &str) -> Result<Option<HashMap<String, String>>> {
let path = SecretPaths::org_user(org_id, user_id);
Ok(self.get_secret(&format!("{}/email", path)).await.ok())
}
/// Get user OAuth credentials
pub async fn get_user_oauth_config(&self, org_id: &str, user_id: &str, provider: &str) -> Result<Option<HashMap<String, String>>> {
let path = SecretPaths::org_user(org_id, user_id);
Ok(self.get_secret(&format!("{}/oauth/{}", path, provider)).await.ok())
}
} }
pub fn init_secrets_manager() -> Result<SecretsManager> { pub fn init_secrets_manager() -> Result<SecretsManager> {