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

@@ -96,11 +96,16 @@ impl PluginService {
// 创建动态表 + 注册 entity 记录
let mut entity_resps = Vec::new();
if let Some(schema) = &manifest.schema {
for entity_def in &schema.entities {
for (i, entity_def) in schema.entities.iter().enumerate() {
let table_name = DynamicTableManager::table_name(&manifest.metadata.id, &entity_def.name);
tracing::info!(step = i, entity = %entity_def.name, table = %table_name, "Creating dynamic table");
// 创建动态表
DynamicTableManager::create_table(db, &manifest.metadata.id, entity_def).await?;
DynamicTableManager::create_table(db, &manifest.metadata.id, entity_def).await
.map_err(|e| {
tracing::error!(entity = %entity_def.name, table = %table_name, error = %e, "Failed to create dynamic table");
e
})?;
// 注册 entity 记录
let entity_id = Uuid::now_v7();
@@ -143,6 +148,7 @@ impl PluginService {
}
// 注册插件声明的权限到 permissions 表
tracing::info!("Registering plugin permissions");
if let Some(perms) = &manifest.permissions {
register_plugin_permissions(
db,
@@ -152,10 +158,15 @@ impl PluginService {
perms,
&now,
)
.await?;
.await
.map_err(|e| {
tracing::error!(error = %e, "Failed to register permissions");
e
})?;
}
// 加载到内存
tracing::info!(manifest_id = %manifest.metadata.id, "Loading plugin into engine");
engine
.load(
&manifest.metadata.id,
@@ -454,7 +465,22 @@ impl PluginService {
let manifest: PluginManifest =
serde_json::from_value(model.manifest_json.clone())
.map_err(|e| PluginError::InvalidManifest(e.to_string()))?;
Ok(serde_json::to_value(&manifest.schema).unwrap_or_default())
// 构建 schema 响应entities + ui 页面配置
let mut result = serde_json::Map::new();
if let Some(schema) = &manifest.schema {
result.insert(
"entities".to_string(),
serde_json::to_value(&schema.entities).unwrap_or_default(),
);
}
if let Some(ui) = &manifest.ui {
result.insert(
"ui".to_string(),
serde_json::to_value(ui).unwrap_or_default(),
);
}
Ok(serde_json::Value::Object(result))
}
/// 清除插件记录(软删除,仅限已卸载状态)
@@ -465,7 +491,7 @@ impl PluginService {
db: &sea_orm::DatabaseConnection,
) -> AppResult<()> {
let model = find_plugin(plugin_id, tenant_id, db).await?;
validate_status(&model.status, "uninstalled")?;
validate_status_any(&model.status, &["uninstalled", "uploaded"])?;
let now = Utc::now();
let mut active: plugin::ActiveModel = model.into();
active.deleted_at = Set(Some(now));
@@ -585,34 +611,33 @@ async fn register_plugin_permissions(
) -> AppResult<()> {
for perm in perms {
let full_code = format!("{}.{}", plugin_manifest_id, perm.code);
// resource 使用插件 manifest idaction 使用权限的 code 字段
let resource = plugin_manifest_id.to_string();
let action = perm.code.clone();
let description_sql = if perm.description.is_empty() {
"NULL".to_string()
let description: Option<String> = if perm.description.is_empty() {
None
} else {
format!("'{}'", perm.description.replace('\'', "''"))
Some(perm.description.clone())
};
let sql = format!(
r#"
let sql = r#"
INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, created_at, updated_at, created_by, updated_by, deleted_at, version)
VALUES (gen_random_uuid(), '{tenant_id}', '{full_code}', '{name}', '{resource}', '{action}', {desc}, '{now}', '{now}', '{operator_id}', '{operator_id}', NULL, 1)
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $7, $8, $8, NULL, 1)
ON CONFLICT (tenant_id, code) WHERE deleted_at IS NULL DO NOTHING
"#,
tenant_id = tenant_id,
full_code = full_code.replace('\'', "''"),
name = perm.name.replace('\'', "''"),
resource = resource.replace('\'', "''"),
action = action.replace('\'', "''"),
desc = description_sql,
now = now.to_rfc3339(),
operator_id = operator_id,
);
"#;
db.execute(sea_orm::Statement::from_string(
db.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
sql,
vec![
sea_orm::Value::from(tenant_id),
sea_orm::Value::from(full_code.clone()),
sea_orm::Value::from(perm.name.clone()),
sea_orm::Value::from(resource),
sea_orm::Value::from(action),
sea_orm::Value::from(description),
sea_orm::Value::from(now.clone()),
sea_orm::Value::from(operator_id),
],
))
.await
.map_err(|e| {
@@ -645,28 +670,28 @@ async fn unregister_plugin_permissions(
plugin_manifest_id: &str,
) -> AppResult<()> {
let prefix = format!("{}.%", plugin_manifest_id);
let now = chrono::Utc::now().to_rfc3339();
let now = chrono::Utc::now();
// 先软删除 role_permissions 中的关联
let rp_sql = format!(
r#"
let rp_sql = r#"
UPDATE role_permissions
SET deleted_at = '{now}', updated_at = '{now}'
SET deleted_at = $1, updated_at = $1
WHERE permission_id IN (
SELECT id FROM permissions
WHERE tenant_id = '{tenant_id}'
AND code LIKE '{prefix}'
WHERE tenant_id = $2
AND code LIKE $3
AND deleted_at IS NULL
)
AND deleted_at IS NULL
"#,
now = now,
tenant_id = tenant_id,
prefix = prefix.replace('\'', "''"),
);
db.execute(sea_orm::Statement::from_string(
"#;
db.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
rp_sql,
vec![
sea_orm::Value::from(now.clone()),
sea_orm::Value::from(tenant_id),
sea_orm::Value::from(prefix.clone()),
],
))
.await
.map_err(|e| {
@@ -679,21 +704,21 @@ async fn unregister_plugin_permissions(
})?;
// 再软删除 permissions
let perm_sql = format!(
r#"
let perm_sql = r#"
UPDATE permissions
SET deleted_at = '{now}', updated_at = '{now}'
WHERE tenant_id = '{tenant_id}'
AND code LIKE '{prefix}'
SET deleted_at = $1, updated_at = $1
WHERE tenant_id = $2
AND code LIKE $3
AND deleted_at IS NULL
"#,
now = now,
tenant_id = tenant_id,
prefix = prefix.replace('\'', "''"),
);
db.execute(sea_orm::Statement::from_string(
"#;
db.execute(sea_orm::Statement::from_sql_and_values(
sea_orm::DatabaseBackend::Postgres,
perm_sql,
vec![
sea_orm::Value::from(now),
sea_orm::Value::from(tenant_id),
sea_orm::Value::from(prefix),
],
))
.await
.map_err(|e| {