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:
@@ -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 id,action 使用权限的 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| {
|
||||
|
||||
Reference in New Issue
Block a user