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

@@ -45,28 +45,25 @@ impl DynamicTableManager {
\"version\" INT NOT NULL DEFAULT 1)"
);
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
create_sql,
))
.await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
db.execute_unprepared(&create_sql).await
.map_err(|e| {
tracing::error!(sql = %create_sql, error = %e, "CREATE TABLE failed");
PluginError::DatabaseError(e.to_string())
})?;
// 创建租户索引
let tenant_idx_sql = format!(
"CREATE INDEX IF NOT EXISTS \"idx_{t}_tenant\" ON \"{table_name}\" (\"tenant_id\") WHERE \"deleted_at\" IS NULL",
t = sanitize_identifier(&table_name)
);
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
tenant_idx_sql,
))
.await
db.execute_unprepared(&tenant_idx_sql).await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
// 为字段创建索引(使用参数化方式避免 SQL 注入)
let mut idx_counter = 0usize;
for field in &entity.fields {
if field.unique || field.required {
idx_counter += 1;
let sanitized_field = sanitize_identifier(&field.name);
let idx_name = format!(
"idx_{}_{}_{}",
@@ -77,37 +74,42 @@ impl DynamicTableManager {
// unique 字段使用 CREATE UNIQUE INDEX由数据库保证数据完整性
let idx_sql = if field.unique {
format!(
"CREATE UNIQUE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" (\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
"CREATE UNIQUE INDEX IF NOT EXISTS \"{}\" ON \"{}\" (\"data\"->>'{}') WHERE \"deleted_at\" IS NULL",
idx_name, table_name, sanitized_field
)
} else {
format!(
"CREATE INDEX IF NOT EXISTS \"{idx_name}\" ON \"{table_name}\" (\"data\"->>'{sanitized_field}') WHERE \"deleted_at\" IS NULL"
"CREATE INDEX IF NOT EXISTS \"{}\" ON \"{}\" (\"data\"->>'{}') WHERE \"deleted_at\" IS NULL",
idx_name, table_name, sanitized_field
)
};
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
idx_sql,
))
.await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
tracing::info!(step = idx_counter, field = %field.name, unique = field.unique, sql = %idx_sql, "Creating field index");
match db.execute_unprepared(&idx_sql).await {
Ok(_) => {},
Err(e) => {
tracing::warn!(step = idx_counter, field = %field.name, sql = %idx_sql, error = %e, "Index creation skipped (non-fatal)");
}
}
}
}
// 为 searchable 字段创建 B-tree 索引以加速 ILIKE 前缀查询
for field in &entity.fields {
if field.searchable == Some(true) {
idx_counter += 1;
let sanitized_field = sanitize_identifier(&field.name);
let idx_name = format!("{}_{}_sidx", sanitize_identifier(&table_name), sanitized_field);
let idx_sql = format!(
"CREATE INDEX IF NOT EXISTS \"{}\" ON \"{}\" (\"data\"->>'{}') WHERE \"deleted_at\" IS NULL",
idx_name, table_name, sanitized_field
);
db.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
idx_sql,
))
.await
.map_err(|e| PluginError::DatabaseError(e.to_string()))?;
tracing::info!(step = idx_counter, field = %field.name, sql = %idx_sql, "Creating search index");
match db.execute_unprepared(&idx_sql).await {
Ok(_) => {},
Err(e) => {
tracing::warn!(step = idx_counter, field = %field.name, sql = %idx_sql, error = %e, "Search index creation skipped (non-fatal)");
}
}
}
}
@@ -172,7 +174,7 @@ impl DynamicTableManager {
) -> (String, Vec<Value>) {
let sql = format!(
"INSERT INTO \"{}\" (id, tenant_id, data, created_by, updated_by, version) \
VALUES ($1, $2, $3, $4, $5, 1) \
VALUES ($1, $2, $3::jsonb, $4, $5, 1) \
RETURNING id, tenant_id, data, created_at, updated_at, version",
table_name
);
@@ -226,7 +228,7 @@ impl DynamicTableManager {
) -> (String, Vec<Value>) {
let sql = format!(
"UPDATE \"{}\" \
SET data = $1, updated_at = NOW(), updated_by = $2, version = version + 1 \
SET data = $1::jsonb, updated_at = NOW(), updated_by = $2, version = version + 1 \
WHERE id = $3 AND tenant_id = $4 AND version = $5 AND deleted_at IS NULL \
RETURNING id, data, created_at, updated_at, version",
table_name