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
294 lines
9.5 KiB
Rust
294 lines
9.5 KiB
Rust
//! 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())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|