Files
zclaw_openfang/crates/zclaw-saas/src/state.rs
iven e3b93ff96d fix(security): implement all 15 security fixes from penetration test V1
Security audit (2026-03-31): 5 HIGH + 10 MEDIUM issues, all fixed.

HIGH:
- H1: JWT password_version mechanism (pwv in Claims, middleware verification,
  auto-increment on password change)
- H2: Docker saas port bound to 127.0.0.1
- H3: TOTP encryption key decoupled from JWT secret (production bailout)
- H4+H5: Tauri CSP hardened (removed unsafe-inline, restricted connect-src)

MEDIUM:
- M1: Persistent rate limiting (PostgreSQL rate_limit_events table)
- M2: Account lockout (5 failures -> 15min lock)
- M3: RFC 5322 email validation with regex
- M4: Device registration typed struct with length limits
- M5: Provider URL validation on create/update (SSRF prevention)
- M6: Legacy TOTP secret migration (fixed nonce -> random nonce)
- M7: Legacy frontend crypto migration (static salt -> random salt)
- M8+M9: Admin frontend: removed JS token storage, HttpOnly cookie only
- M10: Pipeline debug log sanitization (keys only, 100-char truncation)

Also: fixed CLAUDE.md Section 12 (was corrupted), added title.rs middleware
skeleton, fixed RegisterDeviceRequest visibility.
2026-04-01 08:38:37 +08:00

100 lines
3.7 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.

//! 应用状态
use sqlx::PgPool;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Instant;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
use crate::config::SaaSConfig;
use crate::workers::WorkerDispatcher;
use crate::cache::AppCache;
/// 全局应用状态,通过 Axum State 共享
#[derive(Clone)]
pub struct AppState {
/// 数据库连接池
pub db: PgPool,
/// 服务器配置 (可热更新)
pub config: Arc<RwLock<SaaSConfig>>,
/// JWT 密钥
pub jwt_secret: secrecy::SecretString,
/// 速率限制: account_id → 请求时间戳列表
pub rate_limit_entries: Arc<dashmap::DashMap<String, Vec<Instant>>>,
/// 角色权限缓存: role_id → permissions list
pub role_permissions_cache: Arc<dashmap::DashMap<String, Vec<String>>>,
/// TOTP 失败计数: account_id → (失败次数, 首次失败时间)
pub totp_fail_counts: Arc<dashmap::DashMap<String, (u32, Instant)>>,
/// 无锁 rate limit RPM从 config 同步,避免每个请求获取 RwLock
rate_limit_rpm: Arc<AtomicU32>,
/// Worker 调度器 (异步后台任务)
pub worker_dispatcher: WorkerDispatcher,
/// 优雅停机令牌 — 触发后所有 SSE 流和长连接应立即终止
pub shutdown_token: CancellationToken,
/// 应用缓存: Model/Provider/队列计数器
pub cache: AppCache,
}
impl AppState {
pub fn new(db: PgPool, config: SaaSConfig, worker_dispatcher: WorkerDispatcher, shutdown_token: CancellationToken) -> anyhow::Result<Self> {
let jwt_secret = config.jwt_secret()?;
let rpm = config.rate_limit.requests_per_minute;
Ok(Self {
db,
config: Arc::new(RwLock::new(config)),
jwt_secret,
rate_limit_entries: Arc::new(dashmap::DashMap::new()),
role_permissions_cache: Arc::new(dashmap::DashMap::new()),
totp_fail_counts: Arc::new(dashmap::DashMap::new()),
rate_limit_rpm: Arc::new(AtomicU32::new(rpm)),
worker_dispatcher,
shutdown_token,
cache: AppCache::new(),
})
}
/// 获取当前 rate limit RPM无锁读取
pub fn rate_limit_rpm(&self) -> u32 {
self.rate_limit_rpm.load(Ordering::Relaxed)
}
/// 更新 rate limit RPM配置热更新时调用
pub fn set_rate_limit_rpm(&self, rpm: u32) {
self.rate_limit_rpm.store(rpm, Ordering::Relaxed);
}
/// 清理过期的限流条目
/// 使用 3600s 窗口以覆盖 register rate limit (3次/小时) 的完整周期
pub fn cleanup_rate_limit_entries(&self) {
let window_start = Instant::now() - std::time::Duration::from_secs(3600);
self.rate_limit_entries.retain(|_, entries| {
entries.retain(|&ts| ts > window_start);
!entries.is_empty()
});
}
/// 异步派发操作日志到 Worker非阻塞
pub async fn dispatch_log_operation(
&self,
account_id: &str,
action: &str,
target_type: &str,
target_id: &str,
details: Option<serde_json::Value>,
ip_address: Option<&str>,
) {
use crate::workers::log_operation::LogOperationArgs;
let args = LogOperationArgs {
account_id: account_id.to_string(),
action: action.to_string(),
target_type: target_type.to_string(),
target_id: target_id.to_string(),
details: details.map(|d| d.to_string()),
ip_address: ip_address.map(|s| s.to_string()),
};
if let Err(e) = self.worker_dispatcher.dispatch("log_operation", args).await {
tracing::warn!("Failed to dispatch log_operation: {}", e);
}
}
}