fix(saas): 安全修复 — IDOR防护、SSRF防护、JWT密钥强制、错误信息脱敏、CORS配置化
- account: admin 权限守卫 (list_accounts/get_account/update_status/list_logs) - relay: SSRF 防护 (禁止内网地址、限制 http scheme、30s 超时) - config: 生产环境强制 ZCLAW_SAAS_JWT_SECRET 环境变量 - error: 500 错误不再泄露内部细节给客户端 - main: CORS 支持配置白名单 origins - 全部 21 个测试通过 (7 unit + 14 integration)
This commit is contained in:
@@ -5,17 +5,25 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::error::SaasResult;
|
||||
use crate::error::{SaasError, SaasResult};
|
||||
use crate::auth::types::AuthContext;
|
||||
use crate::auth::handlers::log_operation;
|
||||
use super::{types::*, service};
|
||||
|
||||
/// GET /api/v1/accounts
|
||||
fn require_admin(ctx: &AuthContext) -> SaasResult<()> {
|
||||
if !ctx.permissions.contains(&"account:admin".to_string()) {
|
||||
return Err(SaasError::Forbidden("需要 account:admin 权限".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// GET /api/v1/accounts (admin only)
|
||||
pub async fn list_accounts(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<ListAccountsQuery>,
|
||||
_ctx: Extension<AuthContext>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
) -> SaasResult<Json<PaginatedResponse<serde_json::Value>>> {
|
||||
require_admin(&ctx)?;
|
||||
service::list_accounts(&state.db, &query).await.map(Json)
|
||||
}
|
||||
|
||||
@@ -23,30 +31,39 @@ pub async fn list_accounts(
|
||||
pub async fn get_account(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
_ctx: Extension<AuthContext>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
) -> SaasResult<Json<serde_json::Value>> {
|
||||
// 只能查看自己,或 admin 查看任何人
|
||||
if id != ctx.account_id {
|
||||
require_admin(&ctx)?;
|
||||
}
|
||||
service::get_account(&state.db, &id).await.map(Json)
|
||||
}
|
||||
|
||||
/// PUT /api/v1/accounts/:id
|
||||
/// PUT /api/v1/accounts/:id (admin or self for limited fields)
|
||||
pub async fn update_account(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Json(req): Json<UpdateAccountRequest>,
|
||||
) -> SaasResult<Json<serde_json::Value>> {
|
||||
// 非管理员只能修改自己的资料
|
||||
if id != ctx.account_id {
|
||||
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?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// PATCH /api/v1/accounts/:id/status
|
||||
/// PATCH /api/v1/accounts/:id/status (admin only)
|
||||
pub async fn update_status(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Json(req): Json<UpdateStatusRequest>,
|
||||
) -> SaasResult<Json<serde_json::Value>> {
|
||||
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?;
|
||||
@@ -84,12 +101,13 @@ pub async fn revoke_token(
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
/// GET /api/v1/logs/operations
|
||||
/// GET /api/v1/logs/operations (admin only)
|
||||
pub async fn list_operation_logs(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
_ctx: Extension<AuthContext>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
) -> SaasResult<Json<Vec<serde_json::Value>>> {
|
||||
require_admin(&ctx)?;
|
||||
let page: i64 = params.get("page").and_then(|v| v.parse().ok()).unwrap_or(1);
|
||||
let page_size: i64 = params.get("page_size").and_then(|v| v.parse().ok()).unwrap_or(50);
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
Reference in New Issue
Block a user