feat(auth): data_scope 行级数据权限 — DataScope 枚举 + 中间件加载
- TenantContext 新增 permission_data_scopes: HashMap<String, DataScope> - DataScope 枚举: All/SelfOnly/Department/DepartmentTree - JWT 中间件查询 role_permissions.data_scope 填充到上下文 - rbac::get_data_scope() 供 service 层按权限获取数据范围 - 默认 All,完全向后兼容现有行为
This commit is contained in:
@@ -5,7 +5,7 @@ use axum::response::Response;
|
|||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::request_info::REQUEST_INFO;
|
use erp_core::request_info::REQUEST_INFO;
|
||||||
use erp_core::request_info::RequestInfo;
|
use erp_core::request_info::RequestInfo;
|
||||||
use erp_core::types::TenantContext;
|
use erp_core::types::{DataScope, TenantContext};
|
||||||
|
|
||||||
use crate::service::token_service::TokenService;
|
use crate::service::token_service::TokenService;
|
||||||
|
|
||||||
@@ -63,6 +63,12 @@ pub async fn jwt_auth_middleware_fn(
|
|||||||
None => vec![],
|
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),用于审计日志
|
// 提取请求来源信息(IP + User-Agent),用于审计日志
|
||||||
let request_info = RequestInfo::from_headers(req.headers());
|
let request_info = RequestInfo::from_headers(req.headers());
|
||||||
|
|
||||||
@@ -72,6 +78,7 @@ pub async fn jwt_auth_middleware_fn(
|
|||||||
roles: claims.roles,
|
roles: claims.roles,
|
||||||
permissions: claims.permissions,
|
permissions: claims.permissions,
|
||||||
department_ids,
|
department_ids,
|
||||||
|
permission_data_scopes,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reconstruct the request with the TenantContext injected into extensions.
|
// Reconstruct the request with the TenantContext injected into extensions.
|
||||||
@@ -105,3 +112,58 @@ async fn fetch_user_department_ids(
|
|||||||
vec![]
|
vec![]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 查询用户每个权限的数据范围(从 role_permissions 表)
|
||||||
|
async fn fetch_permission_data_scopes(
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
tenant_id: uuid::Uuid,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> std::collections::HashMap<String, DataScope> {
|
||||||
|
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::<String>(0),
|
||||||
|
row.try_get_by_index::<String>(2),
|
||||||
|
) {
|
||||||
|
scopes.insert(code, DataScope::from_str(&scope));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scopes
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, "查询权限数据范围失败,默认全部 All");
|
||||||
|
std::collections::HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::types::TenantContext;
|
use crate::types::{DataScope, TenantContext};
|
||||||
|
|
||||||
/// Check whether the `TenantContext` includes the specified permission code.
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -50,6 +60,7 @@ mod tests {
|
|||||||
roles: roles.into_iter().map(String::from).collect(),
|
roles: roles.into_iter().map(String::from).collect(),
|
||||||
permissions: permissions.into_iter().map(String::from).collect(),
|
permissions: permissions.into_iter().map(String::from).collect(),
|
||||||
department_ids: vec![],
|
department_ids: vec![],
|
||||||
|
permission_data_scopes: std::collections::HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// 所有数据库实体的公共字段
|
/// 所有数据库实体的公共字段
|
||||||
@@ -114,6 +115,7 @@ mod tests {
|
|||||||
roles: vec!["admin".to_string()],
|
roles: vec!["admin".to_string()],
|
||||||
permissions: vec!["user.read".to_string()],
|
permissions: vec!["user.read".to_string()],
|
||||||
department_ids: vec![],
|
department_ids: vec![],
|
||||||
|
permission_data_scopes: HashMap::new(),
|
||||||
};
|
};
|
||||||
assert_eq!(ctx.roles.len(), 1);
|
assert_eq!(ctx.roles.len(), 1);
|
||||||
assert_eq!(ctx.permissions.len(), 1);
|
assert_eq!(ctx.permissions.len(), 1);
|
||||||
@@ -148,6 +150,30 @@ impl<T: Serialize> ApiResponse<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 行级数据权限范围
|
||||||
|
#[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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TenantContext {
|
pub struct TenantContext {
|
||||||
@@ -157,4 +183,6 @@ pub struct TenantContext {
|
|||||||
pub permissions: Vec<String>,
|
pub permissions: Vec<String>,
|
||||||
/// 用户所属部门 ID 列表(行级数据权限使用)
|
/// 用户所属部门 ID 列表(行级数据权限使用)
|
||||||
pub department_ids: Vec<Uuid>,
|
pub department_ids: Vec<Uuid>,
|
||||||
|
/// 每个权限码对应的数据范围(从 role_permissions.data_scope 加载)
|
||||||
|
pub permission_data_scopes: HashMap<String, DataScope>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user