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.
100 lines
3.7 KiB
Rust
100 lines
3.7 KiB
Rust
//! 应用状态
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|