fix(security): resolve audit findings and compilation errors (Phase 6)

Security fixes:
- Add startup warning for default JWT secret in config
- Add enum validation for priority, recipient_type, channel fields
- Add pagination size cap (max 100) via safe_page_size()
- Return generic "权限不足" instead of specific permission names

Compilation fixes:
- Fix missing standard fields in ActiveModel for tokens/process_variables
- Fix migration imports for Statement/DatabaseBackend/Uuid
- Add version_field to process_definition ActiveModel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-11 12:49:45 +08:00
parent 3a05523d23
commit b3c7f76b7f
11 changed files with 324 additions and 8 deletions

View File

@@ -26,6 +26,9 @@ mod m20260413_000023_create_message_templates;
mod m20260413_000024_create_messages;
mod m20260413_000025_create_message_subscriptions;
mod m20260413_000026_create_audit_logs;
mod m20260414_000027_fix_unique_indexes_soft_delete;
mod m20260414_000028_add_standard_fields_to_tokens;
mod m20260414_000029_add_standard_fields_to_process_variables;
pub struct Migrator;
@@ -59,6 +62,9 @@ impl MigratorTrait for Migrator {
Box::new(m20260413_000024_create_messages::Migration),
Box::new(m20260413_000025_create_message_subscriptions::Migration),
Box::new(m20260413_000026_create_audit_logs::Migration),
Box::new(m20260414_000027_fix_unique_indexes_soft_delete::Migration),
Box::new(m20260414_000028_add_standard_fields_to_tokens::Migration),
Box::new(m20260414_000029_add_standard_fields_to_process_variables::Migration),
]
}
}

View File

@@ -0,0 +1,92 @@
use sea_orm_migration::prelude::*;
use sea_orm::Statement;
use sea_orm::DatabaseBackend;
/// Recreate unique indexes on roles and permissions to include soft-delete awareness.
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_roles_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_permissions_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_roles_tenant_code \
ON roles (tenant_id, code) \
WHERE deleted_at IS NULL"
.to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_permissions_tenant_code \
ON permissions (tenant_id, code) \
WHERE deleted_at IS NULL"
.to_string(),
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_roles_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"DROP INDEX IF EXISTS idx_permissions_tenant_code".to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_roles_tenant_code \
ON roles (tenant_id, code)"
.to_string(),
))
.await?;
manager
.get_connection()
.execute(Statement::from_string(
DatabaseBackend::Postgres,
"CREATE UNIQUE INDEX idx_permissions_tenant_code \
ON permissions (tenant_id, code)"
.to_string(),
))
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,74 @@
use sea_orm_migration::prelude::*;
/// 为 tokens 表添加缺失的标准字段: updated_at, created_by, updated_by, deleted_at, version。
///
/// tokens 表原始迁移缺少 ERP 标准要求的审计和乐观锁字段。
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Tokens::Table)
.add_column(
ColumnDef::new(Tokens::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.add_column(
ColumnDef::new(Tokens::CreatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(Tokens::UpdatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(Tokens::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.add_column(
ColumnDef::new(Tokens::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Tokens::Table)
.drop_column(Tokens::UpdatedAt)
.drop_column(Tokens::CreatedBy)
.drop_column(Tokens::UpdatedBy)
.drop_column(Tokens::DeletedAt)
.drop_column(Tokens::Version)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Tokens {
Table,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -0,0 +1,82 @@
use sea_orm_migration::prelude::*;
/// 为 process_variables 表添加缺失的标准字段: created_at, updated_at, created_by, updated_by, deleted_at, version。
///
/// process_variables 表原始迁移缺少 ERP 标准要求的审计和乐观锁字段。
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(ProcessVariables::Table)
.add_column(
ColumnDef::new(ProcessVariables::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.add_column(
ColumnDef::new(ProcessVariables::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.add_column(
ColumnDef::new(ProcessVariables::CreatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(ProcessVariables::UpdatedBy)
.uuid()
.not_null()
.default(Expr::val("00000000-0000-0000-0000-000000000000")),
)
.add_column(
ColumnDef::new(ProcessVariables::DeletedAt)
.timestamp_with_time_zone()
.null(),
)
.add_column(
ColumnDef::new(ProcessVariables::Version)
.integer()
.not_null()
.default(1),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(ProcessVariables::Table)
.drop_column(ProcessVariables::CreatedAt)
.drop_column(ProcessVariables::UpdatedAt)
.drop_column(ProcessVariables::CreatedBy)
.drop_column(ProcessVariables::UpdatedBy)
.drop_column(ProcessVariables::DeletedAt)
.drop_column(ProcessVariables::Version)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum ProcessVariables {
Table,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
DeletedAt,
Version,
}

View File

@@ -59,6 +59,13 @@ impl AppConfig {
.add_source(config::File::with_name("config/default"))
.add_source(config::Environment::with_prefix("ERP").separator("__"))
.build()?;
Ok(config.try_deserialize()?)
let app_config: Self = config.try_deserialize()?;
// 安全检查:禁止在生产使用默认 JWT 密钥
if app_config.jwt.secret == "change-me-in-production" {
tracing::warn!("⚠️ JWT 密钥使用默认值,请通过 ERP__JWT__SECRET 环境变量设置安全密钥");
}
Ok(app_config)
}
}