fix(plugin): 修复插件 schema API、动态表 JSONB 和 SQL 注入防护

- get_schema 端点同时返回 entities 和 ui 页面配置,修复前端无法生成动态菜单的问题
- 动态表 INSERT/UPDATE 添加 ::jsonb 类型转换,修复 PostgreSQL 类型推断错误
- JSONB 索引创建改为非致命(warn 跳过),避免索引冲突阻断安装流程
- 权限注册/注销改用参数化查询,消除 SQL 注入风险
- DDL 语句改用 execute_unprepared,避免不必要的安全检查开销
- clear_plugin 支持已上传状态的清理
- 添加关键步骤 tracing 日志便于排查安装问题
This commit is contained in:
iven
2026-04-16 23:42:40 +08:00
parent b482230a07
commit 3483395f5e
6 changed files with 225 additions and 176 deletions

View File

@@ -6,12 +6,28 @@ use erp_core::events::EventBus;
use crate::data_dto::PluginDataResp;
use crate::dynamic_table::DynamicTableManager;
use crate::entity::plugin;
use crate::entity::plugin_entity;
use crate::error::PluginError;
use crate::manifest::PluginField;
pub struct PluginDataService;
/// 插件实体信息(合并查询减少 DB 调用)
struct EntityInfo {
table_name: String,
schema_json: serde_json::Value,
}
impl EntityInfo {
fn fields(&self) -> AppResult<Vec<PluginField>> {
let entity_def: crate::manifest::PluginEntity =
serde_json::from_value(self.schema_json.clone())
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
Ok(entity_def.fields)
}
}
impl PluginDataService {
/// 创建插件数据
pub async fn create(
@@ -23,13 +39,12 @@ impl PluginDataService {
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<PluginDataResp> {
// 数据校验
let fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
let fields = info.fields()?;
validate_data(&data, &fields)?;
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) =
DynamicTableManager::build_insert_sql(&table_name, tenant_id, operator_id, &data);
DynamicTableManager::build_insert_sql(&info.table_name, tenant_id, operator_id, &data);
#[derive(FromQueryResult)]
struct InsertResult {
@@ -71,10 +86,10 @@ impl PluginDataService {
sort_by: Option<String>,
sort_order: Option<String>,
) -> AppResult<(Vec<PluginDataResp>, u64)> {
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
// 获取 searchable 字段列表
let entity_fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
let entity_fields = info.fields()?;
let search_tuple = {
let searchable: Vec<&str> = entity_fields
.iter()
@@ -87,9 +102,9 @@ impl PluginDataService {
}
};
// Count(使用基础条件,不含 filter/search 以保持计数一致性)
// Count
let (count_sql, count_values) =
DynamicTableManager::build_count_sql(&table_name, tenant_id);
DynamicTableManager::build_count_sql(&info.table_name, tenant_id);
#[derive(FromQueryResult)]
struct CountResult {
count: i64,
@@ -104,10 +119,10 @@ impl PluginDataService {
.map(|r| r.count as u64)
.unwrap_or(0);
// Query(带过滤/搜索/排序)
// Query
let offset = page.saturating_sub(1) * page_size;
let (sql, values) = DynamicTableManager::build_filtered_query_sql(
&table_name,
&info.table_name,
tenant_id,
page_size,
offset,
@@ -157,8 +172,8 @@ impl PluginDataService {
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<PluginDataResp> {
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_get_by_id_sql(&table_name, id, tenant_id);
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_get_by_id_sql(&info.table_name, id, tenant_id);
#[derive(FromQueryResult)]
struct DataRow {
@@ -199,13 +214,12 @@ impl PluginDataService {
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<PluginDataResp> {
// 数据校验
let fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
let fields = info.fields()?;
validate_data(&data, &fields)?;
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_update_sql(
&table_name,
&info.table_name,
id,
tenant_id,
operator_id,
@@ -249,8 +263,8 @@ impl PluginDataService {
db: &sea_orm::DatabaseConnection,
_event_bus: &EventBus,
) -> AppResult<()> {
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_delete_sql(&table_name, id, tenant_id);
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_delete_sql(&info.table_name, id, tenant_id);
db.execute(Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
@@ -271,10 +285,9 @@ impl PluginDataService {
filter: Option<serde_json::Value>,
search: Option<String>,
) -> AppResult<u64> {
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
// 获取 searchable 字段列表,构建搜索条件
let entity_fields = resolve_entity_fields(plugin_id, entity_name, tenant_id, db).await?;
let entity_fields = info.fields()?;
let search_tuple = {
let searchable: Vec<&str> = entity_fields
.iter()
@@ -288,7 +301,7 @@ impl PluginDataService {
};
let (sql, values) = DynamicTableManager::build_filtered_count_sql(
&table_name,
&info.table_name,
tenant_id,
filter,
search_tuple,
@@ -323,10 +336,10 @@ impl PluginDataService {
group_by_field: &str,
filter: Option<serde_json::Value>,
) -> AppResult<Vec<(String, i64)>> {
let table_name = resolve_table_name(plugin_id, entity_name, tenant_id, db).await?;
let info = resolve_entity_info(plugin_id, entity_name, tenant_id, db).await?;
let (sql, values) = DynamicTableManager::build_aggregate_sql(
&table_name,
&info.table_name,
tenant_id,
group_by_field,
filter,
@@ -356,13 +369,34 @@ impl PluginDataService {
}
}
/// 从 plugin_entities 表解析 table_name带租户隔离
async fn resolve_table_name(
/// 从 plugins 表解析 manifest metadata.id如 "erp-crm"
pub async fn resolve_manifest_id(
plugin_id: Uuid,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<String> {
let model = plugin::Entity::find()
.filter(plugin::Column::Id.eq(plugin_id))
.filter(plugin::Column::TenantId.eq(tenant_id))
.filter(plugin::Column::DeletedAt.is_null())
.one(db)
.await?
.ok_or_else(|| AppError::NotFound(format!("插件 {} 不存在", plugin_id)))?;
let manifest: crate::manifest::PluginManifest =
serde_json::from_value(model.manifest_json)
.map_err(|e| AppError::Internal(format!("解析插件 manifest 失败: {}", e)))?;
Ok(manifest.metadata.id)
}
/// 从 plugin_entities 表获取实体完整信息(带租户隔离)
async fn resolve_entity_info(
plugin_id: Uuid,
entity_name: &str,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<String> {
) -> AppResult<EntityInfo> {
let entity = plugin_entity::Entity::find()
.filter(plugin_entity::Column::PluginId.eq(plugin_id))
.filter(plugin_entity::Column::TenantId.eq(tenant_id))
@@ -374,32 +408,10 @@ async fn resolve_table_name(
AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name))
})?;
Ok(entity.table_name)
}
/// 从 plugin_entities 表获取 entity 的字段定义
async fn resolve_entity_fields(
plugin_id: Uuid,
entity_name: &str,
tenant_id: Uuid,
db: &sea_orm::DatabaseConnection,
) -> AppResult<Vec<PluginField>> {
let entity_model = plugin_entity::Entity::find()
.filter(plugin_entity::Column::PluginId.eq(plugin_id))
.filter(plugin_entity::Column::TenantId.eq(tenant_id))
.filter(plugin_entity::Column::EntityName.eq(entity_name))
.filter(plugin_entity::Column::DeletedAt.is_null())
.one(db)
.await?
.ok_or_else(|| {
AppError::NotFound(format!("插件实体 {}/{} 不存在", plugin_id, entity_name))
})?;
let entity_def: crate::manifest::PluginEntity =
serde_json::from_value(entity_model.schema_json)
.map_err(|e| AppError::Internal(format!("解析 entity schema 失败: {}", e)))?;
Ok(entity_def.fields)
Ok(EntityInfo {
table_name: entity.table_name,
schema_json: entity.schema_json,
})
}
/// 校验数据:检查 required 字段