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