Files
zclaw_openfang/crates/zclaw-saas/src/config.rs
iven bc8c77e7fe fix(security): P0 审计修复 — 6项关键安全/编译问题
F1: kernel.rs multi-agent 编译错误 — 重排 spawn_agent 中 A2A 注册顺序,
    在 config 被 registry.register() 消费前使用
F2: saas-config.toml 从 git 追踪中移除 — 包含数据库密码已进入版本历史
F3: config.rs 硬编码开发密钥改用 #[cfg(debug_assertions)] 编译时门控 —
    dev fallback 密钥不再进入 release 构建
F4: 公共认证端点添加 IP 速率限制 (20 RPM) — 防止暴力破解
F5: SSE relay 路由分离出全局 15s TimeoutLayer — 避免长流式响应被截断
F6: Provider API 密钥入库前 AES-256-GCM 加密 — 明文存储修复

附带:完整审计报告 docs/superpowers/specs/2026-03-30-comprehensive-audit-report.md
2026-03-30 13:32:22 +08:00

294 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! SaaS 服务器配置
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use secrecy::SecretString;
#[cfg(not(debug_assertions))]
use secrecy::ExposeSecret;
#[cfg(not(debug_assertions))]
use sha2::Digest;
/// SaaS 服务器完整配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaaSConfig {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub auth: AuthConfig,
pub relay: RelayConfig,
#[serde(default)]
pub rate_limit: RateLimitConfig,
#[serde(default)]
pub scheduler: SchedulerConfig,
}
/// Scheduler 定时任务配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchedulerConfig {
#[serde(default)]
pub jobs: Vec<JobConfig>,
}
/// 单个定时任务配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobConfig {
pub name: String,
/// 间隔时间,支持 "5m", "1h", "24h", "30s" 格式
pub interval: String,
/// 对应的 Worker 名称
pub task: String,
/// 传递给 Worker 的参数JSON 格式)
#[serde(default)]
pub args: Option<serde_json::Value>,
/// 是否在启动时立即执行
#[serde(default)]
pub run_on_start: bool,
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self { jobs: Vec::new() }
}
}
/// 服务器配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub cors_origins: Vec<String>,
}
/// 数据库配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
#[serde(default = "default_db_url")]
pub url: String,
}
/// 认证配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
#[serde(default = "default_jwt_hours")]
pub jwt_expiration_hours: i64,
#[serde(default = "default_totp_issuer")]
pub totp_issuer: String,
/// Refresh Token 有效期 (小时), 默认 168 小时 = 7 天
#[serde(default = "default_refresh_hours")]
pub refresh_token_hours: i64,
}
/// 中转服务配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelayConfig {
#[serde(default = "default_max_queue")]
pub max_queue_size: usize,
/// 每个 Provider 最大并发请求数 (预留,当前由 max_queue_size 控制)
#[serde(default = "default_max_concurrent")]
pub max_concurrent_per_provider: usize,
/// 批量窗口间隔 (预留,用于请求合并优化)
#[serde(default = "default_batch_window")]
pub batch_window_ms: u64,
#[serde(default = "default_retry_delay")]
pub retry_delay_ms: u64,
#[serde(default = "default_max_attempts")]
pub max_attempts: u32,
}
fn default_host() -> String { "0.0.0.0".into() }
fn default_port() -> u16 { 8080 }
fn default_db_url() -> String { "postgres://localhost:5432/zclaw".into() }
fn default_jwt_hours() -> i64 { 24 }
fn default_totp_issuer() -> String { "ZCLAW SaaS".into() }
fn default_refresh_hours() -> i64 { 168 }
fn default_max_queue() -> usize { 1000 }
fn default_max_concurrent() -> usize { 5 }
fn default_batch_window() -> u64 { 50 }
fn default_retry_delay() -> u64 { 1000 }
fn default_max_attempts() -> u32 { 3 }
/// 速率限制配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
/// 每分钟最大请求数 (滑动窗口)
#[serde(default = "default_rpm")]
pub requests_per_minute: u32,
/// 突发允许的额外请求数
#[serde(default = "default_burst")]
pub burst: u32,
}
fn default_rpm() -> u32 { 60 }
fn default_burst() -> u32 { 10 }
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
requests_per_minute: default_rpm(),
burst: default_burst(),
}
}
}
impl Default for SaaSConfig {
fn default() -> Self {
Self {
server: ServerConfig::default(),
database: DatabaseConfig::default(),
auth: AuthConfig::default(),
relay: RelayConfig::default(),
rate_limit: RateLimitConfig::default(),
scheduler: SchedulerConfig::default(),
}
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
cors_origins: Vec::new(),
}
}
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self { url: default_db_url() }
}
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
jwt_expiration_hours: default_jwt_hours(),
totp_issuer: default_totp_issuer(),
refresh_token_hours: default_refresh_hours(),
}
}
}
impl Default for RelayConfig {
fn default() -> Self {
Self {
max_queue_size: default_max_queue(),
max_concurrent_per_provider: default_max_concurrent(),
batch_window_ms: default_batch_window(),
retry_delay_ms: default_retry_delay(),
max_attempts: default_max_attempts(),
}
}
}
impl SaaSConfig {
/// 加载配置文件,优先级: ZCLAW_SAAS_CONFIG > ZCLAW_ENV > ./saas-config.toml
///
/// ZCLAW_ENV 环境选择:
/// development → config/saas-development.toml
/// production → config/saas-production.toml
/// test → config/saas-test.toml
///
/// ZCLAW_SAAS_CONFIG 指定精确路径(最高优先级)
pub fn load() -> anyhow::Result<Self> {
let config_path = if let Ok(path) = std::env::var("ZCLAW_SAAS_CONFIG") {
PathBuf::from(path)
} else if let Ok(env) = std::env::var("ZCLAW_ENV") {
let filename = format!("config/saas-{}.toml", env);
let path = PathBuf::from(&filename);
if !path.exists() {
anyhow::bail!(
"ZCLAW_ENV={} 指定的配置文件 {} 不存在",
env, filename
);
}
tracing::info!("Loading config for environment: {}", env);
path
} else {
PathBuf::from("saas-config.toml")
};
let mut config = if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
toml::from_str(&content)?
} else {
tracing::warn!("Config file {:?} not found, using defaults", config_path);
SaaSConfig::default()
};
// 环境变量覆盖数据库 URL (避免在配置文件中存储密码)
if let Ok(db_url) = std::env::var("ZCLAW_DATABASE_URL") {
config.database.url = db_url;
}
Ok(config)
}
/// 获取 JWT 密钥 (从环境变量或生成临时值)
/// 生产环境必须设置 ZCLAW_SAAS_JWT_SECRET
pub fn jwt_secret(&self) -> anyhow::Result<SecretString> {
match std::env::var("ZCLAW_SAAS_JWT_SECRET") {
Ok(secret) => Ok(SecretString::from(secret)),
Err(_) => {
// 开发 fallback 密钥仅在 debug 构建中可用,不会进入 release
#[cfg(debug_assertions)]
{
tracing::warn!("ZCLAW_SAAS_JWT_SECRET not set, using development default (INSECURE)");
Ok(SecretString::from("zclaw-dev-only-secret-do-not-use-in-prod".to_string()))
}
#[cfg(not(debug_assertions))]
{
anyhow::bail!(
"ZCLAW_SAAS_JWT_SECRET 环境变量未设置。\
请设置一个强随机密钥 (至少 32 字符)。"
)
}
}
}
}
/// 获取 API Key 加密密钥 (复用 TOTP 加密密钥)
pub fn api_key_encryption_key(&self) -> anyhow::Result<[u8; 32]> {
self.totp_encryption_key()
}
/// 获取 TOTP 加密密钥 (AES-256-GCM, 32 字节)
/// 从 ZCLAW_TOTP_ENCRYPTION_KEY 环境变量加载 (hex 编码的 64 字符)
/// 开发环境使用默认值 (不安全)
pub fn totp_encryption_key(&self) -> anyhow::Result<[u8; 32]> {
match std::env::var("ZCLAW_TOTP_ENCRYPTION_KEY") {
Ok(hex_key) => {
if hex_key.len() != 64 {
anyhow::bail!("ZCLAW_TOTP_ENCRYPTION_KEY 必须是 64 个十六进制字符 (32 字节)");
}
let mut key = [0u8; 32];
for i in 0..32 {
key[i] = u8::from_str_radix(&hex_key[i*2..i*2+2], 16)
.map_err(|_| anyhow::anyhow!("ZCLAW_TOTP_ENCRYPTION_KEY 包含无效的十六进制字符"))?;
}
Ok(key)
}
Err(_) => {
// 开发环境: 仅在 debug 构建中使用固定密钥
#[cfg(debug_assertions)]
{
tracing::warn!("ZCLAW_TOTP_ENCRYPTION_KEY not set, using development default (INSECURE)");
let mut key = [0u8; 32];
key.copy_from_slice(b"zclaw-dev-totp-encrypt-key-32b!x");
Ok(key)
}
#[cfg(not(debug_assertions))]
{
// 生产环境: 使用 JWT 密钥的 SHA-256 哈希作为加密密钥
tracing::warn!("ZCLAW_TOTP_ENCRYPTION_KEY not set, deriving from JWT secret");
let jwt = self.jwt_secret()?;
let hash = sha2::Sha256::digest(jwt.expose_secret().as_bytes());
Ok(hash.into())
}
}
}
}
}