fix(saas): admin_guard_middleware — 非 admin 用户统一返回 403

BUG-M4 修复: 之前非 admin 用户发送 malformed body 到 admin 端点时,
Axum 先反序列化 body 返回 422,绕过了权限检查。

- 新增 admin_guard_middleware (auth/mod.rs) 在中间件层拦截
- account::admin_routes() 拆分 (dashboard 独立)
- billing::admin_routes() + account::admin_routes() 加 guard layer
- 非 admin 用户无论 body 是否合法,统一返回 403
This commit is contained in:
iven
2026-04-17 11:45:55 +08:00
parent b2758d34e9
commit 90340725a4
3 changed files with 35 additions and 2 deletions

View File

@@ -16,8 +16,13 @@ pub fn routes() -> axum::Router<crate::state::AppState> {
.route("/api/v1/tokens", post(handlers::create_token)) .route("/api/v1/tokens", post(handlers::create_token))
.route("/api/v1/tokens/:id", delete(handlers::revoke_token)) .route("/api/v1/tokens/:id", delete(handlers::revoke_token))
.route("/api/v1/logs/operations", get(handlers::list_operation_logs)) .route("/api/v1/logs/operations", get(handlers::list_operation_logs))
.route("/api/v1/admin/dashboard", get(handlers::dashboard_stats))
.route("/api/v1/devices", get(handlers::list_devices)) .route("/api/v1/devices", get(handlers::list_devices))
.route("/api/v1/devices/register", post(handlers::register_device)) .route("/api/v1/devices/register", post(handlers::register_device))
.route("/api/v1/devices/heartbeat", post(handlers::device_heartbeat)) .route("/api/v1/devices/heartbeat", post(handlers::device_heartbeat))
} }
/// Admin-only 路由 (需 admin_guard_middleware 保护)
pub fn admin_routes() -> axum::Router<crate::state::AppState> {
axum::Router::new()
.route("/api/v1/admin/dashboard", get(handlers::dashboard_stats))
}

View File

@@ -203,6 +203,27 @@ pub async fn auth_middleware(
} }
} }
/// Admin 路由守卫中间件: 确保 AuthContext 具有 admin/super_admin 角色
/// 必须在 auth_middleware 之后使用(依赖 Extension<AuthContext>
pub async fn admin_guard_middleware(
mut req: Request,
next: Next,
) -> Response {
use crate::auth::handlers::check_permission;
let ctx = req.extensions().get::<AuthContext>().cloned();
match ctx {
Some(ctx) => {
if let Err(e) = check_permission(&ctx, "account:admin") {
e.into_response()
} else {
next.run(req).await
}
}
None => SaasError::Unauthorized.into_response(),
}
}
/// 路由 (无需认证的端点) /// 路由 (无需认证的端点)
pub fn routes() -> axum::Router<AppState> { pub fn routes() -> axum::Router<AppState> {
use axum::routing::post; use axum::routing::post;

View File

@@ -352,6 +352,10 @@ async fn build_router(state: AppState) -> axum::Router {
let protected_routes = zclaw_saas::auth::protected_routes() let protected_routes = zclaw_saas::auth::protected_routes()
.merge(zclaw_saas::account::routes()) .merge(zclaw_saas::account::routes())
.merge(
zclaw_saas::account::admin_routes()
.layer(middleware::from_fn(zclaw_saas::auth::admin_guard_middleware))
)
.merge(zclaw_saas::model_config::routes()) .merge(zclaw_saas::model_config::routes())
// relay::routes() 不在此合并 — SSE 端点需要更长超时,在最终 Router 单独合并 // relay::routes() 不在此合并 — SSE 端点需要更长超时,在最终 Router 单独合并
.merge(zclaw_saas::migration::routes()) .merge(zclaw_saas::migration::routes())
@@ -361,7 +365,10 @@ async fn build_router(state: AppState) -> axum::Router {
.merge(zclaw_saas::scheduled_task::routes()) .merge(zclaw_saas::scheduled_task::routes())
.merge(zclaw_saas::telemetry::routes()) .merge(zclaw_saas::telemetry::routes())
.merge(zclaw_saas::billing::routes()) .merge(zclaw_saas::billing::routes())
.merge(zclaw_saas::billing::admin_routes()) .merge(
zclaw_saas::billing::admin_routes()
.layer(middleware::from_fn(zclaw_saas::auth::admin_guard_middleware))
)
.merge(zclaw_saas::knowledge::routes()) .merge(zclaw_saas::knowledge::routes())
.merge(zclaw_saas::industry::routes()) .merge(zclaw_saas::industry::routes())
.layer(middleware::from_fn_with_state( .layer(middleware::from_fn_with_state(