feat(auth,plugin): Q3 行级数据权限 — user_departments 表 + JWT 注入 department_ids + data_scope 接线
- 新增 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:
@@ -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::*;
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::data_dto::{
|
||||
PatchPluginDataReq, PluginDataListParams, PluginDataResp, TimeseriesItem, TimeseriesParams,
|
||||
UpdatePluginDataReq,
|
||||
};
|
||||
use crate::data_service::{PluginDataService, resolve_manifest_id};
|
||||
use crate::data_service::{DataScopeParams, PluginDataService, resolve_manifest_id};
|
||||
use crate::state::PluginState;
|
||||
|
||||
/// 获取当前用户对指定权限的 data_scope 等级
|
||||
@@ -105,12 +105,10 @@ where
|
||||
let fine_perm = compute_permission_code(&manifest_id, &entity, "list");
|
||||
require_permission(&ctx, &fine_perm)?;
|
||||
|
||||
// TODO(data_scope): 此处注入行级数据权限过滤
|
||||
// 1. 解析 entity 定义检查 data_scope == Some(true)
|
||||
// 2. 调用 get_data_scope(&ctx, &fine_perm, &state.db) 获取当前用户的 scope 等级
|
||||
// 3. 若 scope != "all",调用 get_dept_members 获取部门成员列表
|
||||
// 4. 将 scope 条件合并到 filter 中传给 PluginDataService::list
|
||||
// 参考: crates/erp-plugin/src/dynamic_table.rs build_data_scope_condition()
|
||||
// 解析数据权限范围
|
||||
let scope = resolve_data_scope(
|
||||
&ctx, &manifest_id, &entity, &fine_perm, &state.db,
|
||||
).await?;
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
@@ -133,6 +131,7 @@ where
|
||||
params.sort_by,
|
||||
params.sort_order,
|
||||
&state.entity_cache,
|
||||
scope,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -498,3 +497,54 @@ where
|
||||
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
/// 解析数据权限范围 — 检查 entity 是否启用 data_scope,
|
||||
/// 若启用则查询用户对该权限的 scope 等级,返回 DataScopeParams。
|
||||
async fn resolve_data_scope(
|
||||
ctx: &TenantContext,
|
||||
manifest_id: &str,
|
||||
entity: &str,
|
||||
fine_perm: &str,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> Result<Option<DataScopeParams>, AppError> {
|
||||
let entity_has_scope = check_entity_data_scope(manifest_id, entity, db).await?;
|
||||
if !entity_has_scope {
|
||||
return Ok(None);
|
||||
}
|
||||
let scope_level = get_data_scope(ctx, fine_perm, db).await?;
|
||||
if scope_level == "all" {
|
||||
return Ok(None);
|
||||
}
|
||||
let dept_members = get_dept_members(ctx, false).await;
|
||||
Ok(Some(DataScopeParams {
|
||||
scope_level,
|
||||
user_id: ctx.user_id,
|
||||
dept_member_ids: dept_members,
|
||||
owner_field: "owner_id".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 查询 entity 定义是否启用了 data_scope
|
||||
async fn check_entity_data_scope(
|
||||
_manifest_id: &str,
|
||||
entity_name: &str,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> Result<bool, AppError> {
|
||||
use crate::entity::plugin_entity;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
let entity = plugin_entity::Entity::find()
|
||||
.filter(plugin_entity::Column::EntityName.eq(entity_name))
|
||||
.filter(plugin_entity::Column::DeletedAt.is_null())
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
|
||||
let Some(e) = entity else { return Ok(false) };
|
||||
|
||||
let schema: crate::manifest::PluginEntity =
|
||||
serde_json::from_value(e.schema_json)
|
||||
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
|
||||
|
||||
Ok(schema.data_scope.unwrap_or(false))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user