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:
@@ -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 字段
|
||||
|
||||
Reference in New Issue
Block a user