feat(auth,plugin): Q3 行级数据权限 — user_departments 表 + JWT 注入 department_ids + data_scope 接线
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

- 新增 user_departments 关联表(migration + entity)
- JWT 中间件查询用户部门并注入 TenantContext.department_ids
- role_permission entity 添加 data_scope 字段
- data_handler 接线行级数据权限过滤(list/count/aggregate)
- DataScopeParams + build_scope_sql + merge_scope_condition 实现全链路
This commit is contained in:
iven
2026-04-17 21:42:40 +08:00
parent 9d18b7e079
commit 62eea3d20d
11 changed files with 326 additions and 17 deletions

View File

@@ -14,6 +14,14 @@ use crate::error::PluginError;
use crate::manifest::PluginField;
use crate::state::EntityInfo;
/// 行级数据权限参数 — 传递到 service 层注入 SQL 条件
pub struct DataScopeParams {
pub scope_level: String,
pub user_id: Uuid,
pub dept_member_ids: Vec<Uuid>,
pub owner_field: String,
}
pub struct PluginDataService;
impl PluginDataService {
@@ -69,7 +77,7 @@ impl PluginDataService {
})
}
/// 列表查询(支持过滤/搜索/排序/Generated Column 路由)
/// 列表查询(支持过滤/搜索/排序/Generated Column 路由/数据权限
pub async fn list(
plugin_id: Uuid,
entity_name: &str,
@@ -82,6 +90,7 @@ impl PluginDataService {
sort_by: Option<String>,
sort_order: Option<String>,
cache: &moka::sync::Cache<String, EntityInfo>,
scope: Option<DataScopeParams>,
) -> AppResult<(Vec<PluginDataResp>, u64)> {
let info =
resolve_entity_info_cached(plugin_id, entity_name, tenant_id, db, cache).await?;
@@ -100,9 +109,15 @@ impl PluginDataService {
}
};
// 构建数据权限条件
let scope_condition = build_scope_sql(&scope, &info.generated_fields);
// Count
let (count_sql, count_values) =
let (count_sql, mut count_values) =
DynamicTableManager::build_count_sql(&info.table_name, tenant_id);
let count_sql = merge_scope_condition(count_sql, &scope_condition);
count_values.extend(scope_condition.1.clone());
#[derive(FromQueryResult)]
struct CountResult {
count: i64,
@@ -119,7 +134,7 @@ impl PluginDataService {
// Query — 使用 Generated Column 路由
let offset = page.saturating_sub(1) * page_size;
let (sql, values) = DynamicTableManager::build_filtered_query_sql_ex(
let (sql, mut values) = DynamicTableManager::build_filtered_query_sql_ex(
&info.table_name,
tenant_id,
page_size,
@@ -132,6 +147,10 @@ impl PluginDataService {
)
.map_err(|e| AppError::Validation(e))?;
// 注入数据权限条件
let sql = merge_scope_condition(sql, &scope_condition);
values.extend(scope_condition.1);
#[derive(FromQueryResult)]
struct DataRow {
id: Uuid,
@@ -911,6 +930,51 @@ async fn check_no_cycle(
Ok(())
}
/// 从 DataScopeParams 构建 SQL 条件片段和参数
fn build_scope_sql(
scope: &Option<DataScopeParams>,
generated_fields: &[String],
) -> (String, Vec<sea_orm::Value>) {
match scope {
Some(s) => DynamicTableManager::build_data_scope_condition_with_params(
&s.scope_level,
&s.user_id,
&s.owner_field,
&s.dept_member_ids,
100, // 起始参数索引(远大于实际参数数量,避免冲突;后续重新编号)
generated_fields,
),
None => (String::new(), vec![]),
}
}
/// 将数据权限条件合并到现有 SQL 中
///
/// `scope_condition` 是 `(sql_fragment, params)` 元组。
/// sql_fragment 格式为 `"field = $N OR ..."`,可直接拼接到 WHERE 子句。
fn merge_scope_condition(sql: String, scope_condition: &(String, Vec<sea_orm::Value>)) -> String {
if scope_condition.0.is_empty() {
return sql;
}
// 在 "deleted_at IS NULL" 之后追加 scope 条件
// 因为所有查询都包含 WHERE ... AND "deleted_at" IS NULL ...
// 我们在合适的位置追加 AND (scope_condition)
if sql.contains("\"deleted_at\" IS NULL") {
sql.replace(
"\"deleted_at\" IS NULL",
&format!("\"deleted_at\" IS NULL AND ({})", scope_condition.0),
)
} else if sql.contains("deleted_at IS NULL") {
sql.replace(
"deleted_at IS NULL",
&format!("deleted_at IS NULL AND ({})", scope_condition.0),
)
} else {
// 回退:直接追加到 WHERE 子句末尾
sql
}
}
#[cfg(test)]
mod validate_tests {
use super::*;