Files
hms/crates/erp-health/src/oauth/middleware.rs
iven 2b90db4028
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(health): P0 安全修复 — SQL注入 + FHIR越权 + OAuth权限 + JWT硬编码
C1: action_inbox_service.rs 中 patient_id/user_id 的 format! 拼接改为
    参数化查询 ($2/$3/$4/$5 绑定),消除 SQL 注入风险
C2: fhir/handler.rs 所有患者相关端点强制执行 allowed_patient_ids 范围
    过滤,search 端点用 is_in 过滤,get 端点用 enforce_patient_scope 校验
H5: oauth/handler.rs 5 个管理端点添加 require_permission 校验
M3: oauth/handler.rs 和 middleware.rs 移除 "dev-secret-key" fallback,
    缺少环境变量时启动失败(token)/返回 500(middleware)
2026-05-04 23:09:25 +08:00

172 lines
5.4 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 axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
/// Client Credentials JWT Claims
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientCredentialsClaims {
/// API 客户端 IDapi_clients 表主键)
pub sub: Uuid,
/// 租户 ID
pub tid: Uuid,
/// 允许的 FHIR scope 列表
pub scopes: Vec<String>,
/// 允许访问的患者 ID 列表None = 该租户下全部患者)
pub allowed_patient_ids: Option<Vec<String>>,
/// 速率限制(每分钟请求数)
pub rate_limit_per_minute: i32,
/// 过期时间
pub exp: i64,
/// 签发时间
pub iat: i64,
/// 令牌类型标识
pub token_type: String,
}
/// FHIR 请求上下文 — 中间件注入到请求扩展中
#[derive(Debug, Clone)]
pub struct FhirAuthContext {
pub client_id: Uuid,
pub tenant_id: Uuid,
pub scopes: Vec<String>,
pub allowed_patient_ids: Option<Vec<String>>,
}
/// FHIR OAuth 认证中间件
pub async fn oauth_auth_middleware(request: Request, next: Next) -> Response {
let auth_header = request
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok());
let token = match auth_header {
Some(header) if header.starts_with("Bearer ") => &header[7..],
_ => {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "login",
"diagnostics": "Missing or invalid Authorization header"
}]
})),
)
.into_response();
}
};
let jwt_secret = match std::env::var("ERP__AUTH__JWT_SECRET") {
Ok(secret) => secret,
Err(_) => {
tracing::error!("ERP__AUTH__JWT_SECRET 环境变量未设置 — 无法验证 OAuth token");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "exception",
"diagnostics": "Server configuration error"
}]
})),
)
.into_response();
}
};
let claims = match jsonwebtoken::decode::<ClientCredentialsClaims>(
token,
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
&jsonwebtoken::Validation::default(),
) {
Ok(data) => data.claims,
Err(e) => {
tracing::warn!(error = %e, "FHIR OAuth token 验证失败");
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "login",
"diagnostics": "Invalid or expired token"
}]
})),
)
.into_response();
}
};
if claims.token_type != "client_credentials" {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"resourceType": "OperationOutcome",
"issue": [{
"severity": "error",
"code": "login",
"diagnostics": "Token is not a client_credentials token"
}]
})),
)
.into_response();
}
let fhir_ctx = FhirAuthContext {
client_id: claims.sub,
tenant_id: claims.tid,
scopes: claims.scopes.clone(),
allowed_patient_ids: claims.allowed_patient_ids,
};
// 同时注入 TenantContext兼容 require_permission() 和 ctx.tenant_id
// 将 FHIR scope 映射为内部 permission code
let permissions: Vec<String> = claims
.scopes
.iter()
.flat_map(|s| scope_to_permissions(s))
.collect();
let tenant_ctx = erp_core::types::TenantContext {
tenant_id: claims.tid,
user_id: claims.sub,
roles: vec!["api_client".to_string()],
permissions,
department_ids: vec![],
permission_data_scopes: HashMap::new(),
};
let mut request = request;
request.extensions_mut().insert(tenant_ctx);
request.extensions_mut().insert(fhir_ctx);
next.run(request).await
}
/// FHIR scope → 内部 permission code 映射
fn scope_to_permissions(scope: &str) -> Vec<String> {
match scope {
"Patient.read" => vec!["health.patient.list".to_string()],
"Observation.read" => vec![
"health.device-readings.list".to_string(),
"health.health-data.list".to_string(),
],
"Device.read" => vec!["health.devices.list".to_string()],
"Practitioner.read" => vec!["health.doctor.list".to_string()],
"Appointment.read" => vec!["health.appointment.list".to_string()],
"DiagnosticReport.read" => vec!["health.health-data.list".to_string()],
"Encounter.read" => vec!["health.consultation.list".to_string()],
"Task.read" => vec!["health.follow-up.list".to_string()],
_ => vec![],
}
}