fix(saas): P0 安全修复 + P1 功能补全 — 角色提升、Admin 引导、IP 记录、密码修改

P0 安全修复:
- 修复 account update 自角色提升漏洞: 非 admin 用户更新自己时剥离 role 字段
- 添加 Admin 引导机制: accounts 表为空时自动从环境变量创建 super_admin

P1 功能补全:
- 所有 17 个 log_operation 调用点传入真实客户端 IP (ConnectInfo + X-Forwarded-For)
- AuthContext 新增 client_ip 字段, middleware 层自动提取
- main.rs 使用 into_make_service_with_connect_info 启用 SocketAddr 注入
- 新增 PUT /api/v1/auth/password 密码修改端点 (验证旧密码 + argon2 哈希)
- 桌面端 SaaS 设置页添加密码修改 UI (折叠式表单)
- SaaSClient 添加 changePassword() 方法
- 集成测试修复: 注入模拟 ConnectInfo 适配 onshot 测试模式
This commit is contained in:
iven
2026-03-27 14:45:47 +08:00
parent 15450ca895
commit 8cce2283f7
11 changed files with 310 additions and 25 deletions

View File

@@ -44,12 +44,25 @@ pub async fn update_account(
Extension(ctx): Extension<AuthContext>,
Json(req): Json<UpdateAccountRequest>,
) -> SaasResult<Json<serde_json::Value>> {
let is_self_update = id == ctx.account_id;
// 非管理员只能修改自己的资料
if id != ctx.account_id {
if !is_self_update {
require_admin(&ctx)?;
}
let result = service::update_account(&state.db, &id, &req).await?;
log_operation(&state.db, &ctx.account_id, "account.update", "account", &id, None, None).await?;
// 安全限制: 非管理员修改自己时,剥离 role 字段防止自角色提升
let safe_req = if is_self_update && !ctx.permissions.contains(&"admin:full".to_string()) {
UpdateAccountRequest {
role: None,
..req
}
} else {
req
};
let result = service::update_account(&state.db, &id, &safe_req).await?;
log_operation(&state.db, &ctx.account_id, "account.update", "account", &id, None, ctx.client_ip.as_deref()).await?;
Ok(Json(result))
}
@@ -63,7 +76,7 @@ pub async fn update_status(
require_admin(&ctx)?;
service::update_account_status(&state.db, &id, &req.status).await?;
log_operation(&state.db, &ctx.account_id, "account.update_status", "account", &id,
Some(serde_json::json!({"status": &req.status})), None).await?;
Some(serde_json::json!({"status": &req.status})), ctx.client_ip.as_deref()).await?;
Ok(Json(serde_json::json!({"ok": true})))
}
@@ -83,7 +96,7 @@ pub async fn create_token(
) -> SaasResult<Json<TokenInfo>> {
let token = service::create_api_token(&state.db, &ctx.account_id, &req).await?;
log_operation(&state.db, &ctx.account_id, "token.create", "api_token", &token.id,
Some(serde_json::json!({"name": &req.name})), None).await?;
Some(serde_json::json!({"name": &req.name})), ctx.client_ip.as_deref()).await?;
Ok(Json(token))
}
@@ -94,7 +107,7 @@ pub async fn revoke_token(
Extension(ctx): Extension<AuthContext>,
) -> SaasResult<Json<serde_json::Value>> {
service::revoke_api_token(&state.db, &id, &ctx.account_id).await?;
log_operation(&state.db, &ctx.account_id, "token.revoke", "api_token", &id, None, None).await?;
log_operation(&state.db, &ctx.account_id, "token.revoke", "api_token", &id, None, ctx.client_ip.as_deref()).await?;
Ok(Json(serde_json::json!({"ok": true})))
}