From 633bf8c62d6a492d3f026fcfcd06783eb22ee17d Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 19:31:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20data=5Fscope=20=E8=A1=8C=E7=BA=A7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=20=E2=80=94=20DataScope=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=20+=20=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TenantContext 新增 permission_data_scopes: HashMap - DataScope 枚举: All/SelfOnly/Department/DepartmentTree - JWT 中间件查询 role_permissions.data_scope 填充到上下文 - rbac::get_data_scope() 供 service 层按权限获取数据范围 - 默认 All,完全向后兼容现有行为 --- crates/erp-auth/src/middleware/jwt_auth.rs | 64 +++++++++++++++++++++- crates/erp-core/src/rbac.rs | 13 ++++- crates/erp-core/src/types.rs | 28 ++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/erp-auth/src/middleware/jwt_auth.rs b/crates/erp-auth/src/middleware/jwt_auth.rs index 4fc1cdc..00a7c94 100644 --- a/crates/erp-auth/src/middleware/jwt_auth.rs +++ b/crates/erp-auth/src/middleware/jwt_auth.rs @@ -5,7 +5,7 @@ use axum::response::Response; use erp_core::error::AppError; use erp_core::request_info::REQUEST_INFO; use erp_core::request_info::RequestInfo; -use erp_core::types::TenantContext; +use erp_core::types::{DataScope, TenantContext}; use crate::service::token_service::TokenService; @@ -63,6 +63,12 @@ pub async fn jwt_auth_middleware_fn( None => vec![], }; + // 查询每个权限的数据范围 + let permission_data_scopes = match &db { + Some(conn) => fetch_permission_data_scopes(claims.sub, claims.tid, conn).await, + None => std::collections::HashMap::new(), + }; + // 提取请求来源信息(IP + User-Agent),用于审计日志 let request_info = RequestInfo::from_headers(req.headers()); @@ -72,6 +78,7 @@ pub async fn jwt_auth_middleware_fn( roles: claims.roles, permissions: claims.permissions, department_ids, + permission_data_scopes, }; // Reconstruct the request with the TenantContext injected into extensions. @@ -105,3 +112,58 @@ async fn fetch_user_department_ids( vec![] }) } + +/// 查询用户每个权限的数据范围(从 role_permissions 表) +async fn fetch_permission_data_scopes( + user_id: uuid::Uuid, + tenant_id: uuid::Uuid, + db: &sea_orm::DatabaseConnection, +) -> std::collections::HashMap { + use sea_orm::ConnectionTrait; + + let sql = r#" + SELECT p.code, MIN( + CASE rp.data_scope + WHEN 'all' THEN 0 + WHEN 'department_tree' THEN 1 + WHEN 'department' THEN 2 + WHEN 'self' THEN 3 + ELSE 0 + END + ) AS scope_rank, + MIN(rp.data_scope) AS data_scope + FROM user_roles ur + JOIN role_permissions rp ON ur.role_id = rp.role_id AND ur.tenant_id = rp.tenant_id + JOIN permissions p ON rp.permission_id = p.id + WHERE ur.user_id = $1 + AND ur.tenant_id = $2 + AND ur.deleted_at IS NULL + AND rp.deleted_at IS NULL + GROUP BY p.code + "#; + + let stmt = sea_orm::Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + sql, + [user_id.into(), tenant_id.into()], + ); + + match db.query_all(stmt).await { + Ok(rows) => { + let mut scopes = std::collections::HashMap::new(); + for row in rows { + if let (Ok(code), Ok(scope)) = ( + row.try_get_by_index::(0), + row.try_get_by_index::(2), + ) { + scopes.insert(code, DataScope::from_str(&scope)); + } + } + scopes + } + Err(e) => { + tracing::warn!(error = %e, "查询权限数据范围失败,默认全部 All"); + std::collections::HashMap::new() + } + } +} diff --git a/crates/erp-core/src/rbac.rs b/crates/erp-core/src/rbac.rs index 4fce2f0..e3b588e 100644 --- a/crates/erp-core/src/rbac.rs +++ b/crates/erp-core/src/rbac.rs @@ -1,5 +1,5 @@ use crate::error::AppError; -use crate::types::TenantContext; +use crate::types::{DataScope, TenantContext}; /// Check whether the `TenantContext` includes the specified permission code. /// @@ -38,6 +38,16 @@ pub fn require_role(ctx: &TenantContext, role: &str) -> Result<(), AppError> { } } +/// 获取指定权限的数据范围。默认 All(向后兼容)。 +/// +/// Service 层根据返回值追加对应的查询过滤条件。 +pub fn get_data_scope(ctx: &TenantContext, permission: &str) -> DataScope { + ctx.permission_data_scopes + .get(permission) + .cloned() + .unwrap_or(DataScope::All) +} + #[cfg(test)] mod tests { use super::*; @@ -50,6 +60,7 @@ mod tests { roles: roles.into_iter().map(String::from).collect(), permissions: permissions.into_iter().map(String::from).collect(), department_ids: vec![], + permission_data_scopes: std::collections::HashMap::new(), } } diff --git a/crates/erp-core/src/types.rs b/crates/erp-core/src/types.rs index 338962e..7145fc0 100644 --- a/crates/erp-core/src/types.rs +++ b/crates/erp-core/src/types.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use uuid::Uuid; /// 所有数据库实体的公共字段 @@ -114,6 +115,7 @@ mod tests { roles: vec!["admin".to_string()], permissions: vec!["user.read".to_string()], department_ids: vec![], + permission_data_scopes: HashMap::new(), }; assert_eq!(ctx.roles.len(), 1); assert_eq!(ctx.permissions.len(), 1); @@ -148,6 +150,30 @@ impl ApiResponse { } } +/// 行级数据权限范围 +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DataScope { + /// 查看所有数据 + All, + /// 仅查看自己创建的数据 + SelfOnly, + /// 仅查看本部门数据 + Department, + /// 查看本部门及下属部门数据 + DepartmentTree, +} + +impl DataScope { + pub fn from_str(s: &str) -> Self { + match s { + "self" => Self::SelfOnly, + "department" => Self::Department, + "department_tree" => Self::DepartmentTree, + _ => Self::All, + } + } +} + /// 租户上下文(中间件注入) #[derive(Debug, Clone)] pub struct TenantContext { @@ -157,4 +183,6 @@ pub struct TenantContext { pub permissions: Vec, /// 用户所属部门 ID 列表(行级数据权限使用) pub department_ids: Vec, + /// 每个权限码对应的数据范围(从 role_permissions.data_scope 加载) + pub permission_data_scopes: HashMap, }