From 0cbd08eb788b51e3679070f1c43eafb824103bba Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 11 Apr 2026 08:26:43 +0800 Subject: [PATCH] fix(config): resolve critical audit findings from Phase 1-3 review - C-1: Add tenant_id to settings unique index to prevent cross-tenant conflicts - C-2: Move pg_advisory_xact_lock inside the transaction for correct concurrency (previously lock was released before the numbering transaction started) - H-5: Add CORS middleware (permissive for dev, TODO: restrict in production) --- .../src/service/numbering_service.rs | 31 ++++++++++--------- .../src/m20260412_000016_create_settings.rs | 2 +- crates/erp-server/src/main.rs | 6 +++- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/crates/erp-config/src/service/numbering_service.rs b/crates/erp-config/src/service/numbering_service.rs index 6bd31a7..91eb431 100644 --- a/crates/erp-config/src/service/numbering_service.rs +++ b/crates/erp-config/src/service/numbering_service.rs @@ -204,8 +204,8 @@ impl NumberingService { /// 线程安全地生成编号。 /// /// 使用 PostgreSQL advisory lock 保证并发安全: - /// 1. 获取 pg_advisory_xact_lock - /// 2. 在事务内读取规则、检查重置周期、递增序列、更新数据库 + /// 1. 在事务内获取 pg_advisory_xact_lock + /// 2. 在同一事务内读取规则、检查重置周期、递增序列、更新数据库 /// 3. 拼接编号字符串返回 pub async fn generate_number( rule_id: Uuid, @@ -223,22 +223,23 @@ impl NumberingService { let rule_code = rule.code.clone(); let tenant_id_str = tenant_id.to_string(); - // 获取 PostgreSQL advisory lock(事务级别,事务结束自动释放) - db.execute(Statement::from_sql_and_values( - DatabaseBackend::Postgres, - "SELECT pg_advisory_xact_lock(abs(hashtext($1)), abs(hashtext($2))::int)", - [ - rule_code.into(), - tenant_id_str.into(), - ], - )) - .await - .map_err(|e| ConfigError::Validation(format!("获取编号锁失败: {e}")))?; - - // 在事务内执行序列递增和更新 + // 在同一个事务内获取 advisory lock 并执行编号生成 + // pg_advisory_xact_lock 是事务级别的,锁会在事务结束时自动释放 let number = db .transaction(|txn| { + let rule_code = rule_code.clone(); + let tenant_id_str = tenant_id_str.clone(); Box::pin(async move { + // 在事务内获取 advisory lock + txn.execute(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + "SELECT pg_advisory_xact_lock(abs(hashtext($1)), abs(hashtext($2))::int)", + [rule_code.into(), tenant_id_str.into()], + )) + .await + .map_err(|e| ConfigError::Validation(format!("获取编号锁失败: {e}")))?; + + // 在同一个事务内执行编号生成 Self::generate_number_in_txn(rule_id, tenant_id, txn).await }) }) diff --git a/crates/erp-server/migration/src/m20260412_000016_create_settings.rs b/crates/erp-server/migration/src/m20260412_000016_create_settings.rs index 9bc3f05..4517ca2 100644 --- a/crates/erp-server/migration/src/m20260412_000016_create_settings.rs +++ b/crates/erp-server/migration/src/m20260412_000016_create_settings.rs @@ -68,7 +68,7 @@ impl MigrationTrait for Migration { manager.get_connection().execute(sea_orm::Statement::from_string( sea_orm::DatabaseBackend::Postgres, - "CREATE UNIQUE INDEX idx_settings_scope_key ON settings (scope, scope_id, setting_key) WHERE deleted_at IS NULL".to_string(), + "CREATE UNIQUE INDEX idx_settings_scope_key ON settings (tenant_id, scope, scope_id, setting_key) WHERE deleted_at IS NULL".to_string(), )).await.map_err(|e| DbErr::Custom(e.to_string()))?; Ok(()) diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index ab0f466..7e51ca5 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -153,7 +153,11 @@ async fn main() -> anyhow::Result<()> { .with_state(state.clone()); // Merge public + protected into the final application router - let app = Router::new().merge(public_routes).merge(protected_routes); + let cors = tower_http::cors::CorsLayer::permissive(); // TODO: restrict origins in production + let app = Router::new() + .merge(public_routes) + .merge(protected_routes) + .layer(cors); let addr = format!("{}:{}", host, port); let listener = tokio::net::TcpListener::bind(&addr).await?;