fix(security): Q2 Chunk 1 — 密钥外部化与启动强制检查
- default.toml 敏感值改为占位符,强制通过环境变量注入 - 启动时拒绝默认 JWT 密钥和数据库 URL - 移除 super_admin_password 硬编码 fallback - 移除 From<AppError> for AuthError 反向映射,5 处调用点改为显式 map_err - .gitignore 添加 .test_token 和测试产物
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -25,3 +25,13 @@ Thumbs.db
|
|||||||
# Docker data
|
# Docker data
|
||||||
docker/postgres_data/
|
docker/postgres_data/
|
||||||
docker/redis_data/
|
docker/redis_data/
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
.test_token
|
||||||
|
*.heapsnapshot
|
||||||
|
perf-trace-*.json
|
||||||
|
docs/debug-*.png
|
||||||
|
|
||||||
|
# Development env
|
||||||
|
.env.development
|
||||||
|
docker/docker-compose.override.yml
|
||||||
|
|||||||
@@ -43,15 +43,6 @@ impl From<AuthError> for AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AppError> for AuthError {
|
|
||||||
fn from(err: AppError) -> Self {
|
|
||||||
match err {
|
|
||||||
AppError::VersionMismatch => AuthError::VersionMismatch,
|
|
||||||
other => AuthError::Validation(other.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type AuthResult<T> = Result<T, AuthError>;
|
pub type AuthResult<T> = Result<T, AuthError>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -104,28 +95,4 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn auth_error_version_mismatch_roundtrip() {
|
|
||||||
// AuthError::VersionMismatch -> AppError::VersionMismatch -> AuthError::VersionMismatch
|
|
||||||
let app: AppError = AuthError::VersionMismatch.into();
|
|
||||||
match app {
|
|
||||||
AppError::VersionMismatch => {}
|
|
||||||
other => panic!("Expected VersionMismatch, got {:?}", other),
|
|
||||||
}
|
|
||||||
// And back
|
|
||||||
let auth: AuthError = AppError::VersionMismatch.into();
|
|
||||||
match auth {
|
|
||||||
AuthError::VersionMismatch => {}
|
|
||||||
other => panic!("Expected VersionMismatch, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn app_error_other_maps_to_auth_validation() {
|
|
||||||
let auth: AuthError = AppError::NotFound("not found".to_string()).into();
|
|
||||||
match auth {
|
|
||||||
AuthError::Validation(msg) => assert!(msg.contains("not found")),
|
|
||||||
other => panic!("Expected Validation, got {:?}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,12 @@ impl ErpModule for AuthModule {
|
|||||||
_event_bus: &EventBus,
|
_event_bus: &EventBus,
|
||||||
) -> AppResult<()> {
|
) -> AppResult<()> {
|
||||||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
||||||
.unwrap_or_else(|_| "Admin@2026".to_string());
|
.map_err(|_| {
|
||||||
|
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
|
||||||
|
erp_core::error::AppError::Internal(
|
||||||
|
"ERP__SUPER_ADMIN_PASSWORD 未设置".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
|
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ impl DeptService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_ver = check_version(req.version, model.version)?;
|
let next_ver = check_version(req.version, model.version)
|
||||||
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
let mut active: department::ActiveModel = model.into();
|
let mut active: department::ActiveModel = model.into();
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ impl OrgService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_ver = check_version(req.version, model.version)?;
|
let next_ver = check_version(req.version, model.version)
|
||||||
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
let mut active: organization::ActiveModel = model.into();
|
let mut active: organization::ActiveModel = model.into();
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ impl PositionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_ver = check_version(req.version, model.version)?;
|
let next_ver = check_version(req.version, model.version)
|
||||||
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
let mut active: position::ActiveModel = model.into();
|
let mut active: position::ActiveModel = model.into();
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ impl RoleService {
|
|||||||
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
|
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
|
||||||
.ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?;
|
.ok_or_else(|| AuthError::Validation("角色不存在".to_string()))?;
|
||||||
|
|
||||||
let next_ver = check_version(version, model.version)?;
|
let next_ver = check_version(version, model.version)
|
||||||
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
let mut active: role::ActiveModel = model.into();
|
let mut active: role::ActiveModel = model.into();
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,8 @@ impl UserService {
|
|||||||
.filter(|u| u.tenant_id == tenant_id && u.deleted_at.is_none())
|
.filter(|u| u.tenant_id == tenant_id && u.deleted_at.is_none())
|
||||||
.ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?;
|
.ok_or_else(|| AuthError::Validation("用户不存在".to_string()))?;
|
||||||
|
|
||||||
let next_ver = check_version(req.version, user_model.version)?;
|
let next_ver = check_version(req.version, user_model.version)
|
||||||
|
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
let mut active: user::ActiveModel = user_model.into();
|
let mut active: user::ActiveModel = user_model.into();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ host = "0.0.0.0"
|
|||||||
port = 3000
|
port = 3000
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
url = "postgres://erp:erp_dev_2024@localhost:5432/erp"
|
url = "__MUST_SET_VIA_ENV__"
|
||||||
max_connections = 20
|
max_connections = 20
|
||||||
min_connections = 5
|
min_connections = 5
|
||||||
|
|
||||||
@@ -11,12 +11,12 @@ min_connections = 5
|
|||||||
url = "redis://localhost:6379"
|
url = "redis://localhost:6379"
|
||||||
|
|
||||||
[jwt]
|
[jwt]
|
||||||
secret = "change-me-in-production"
|
secret = "__MUST_SET_VIA_ENV__"
|
||||||
access_token_ttl = "15m"
|
access_token_ttl = "15m"
|
||||||
refresh_token_ttl = "7d"
|
refresh_token_ttl = "7d"
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
super_admin_password = "Admin@2026"
|
super_admin_password = "__MUST_SET_VIA_ENV__"
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "info"
|
level = "info"
|
||||||
|
|||||||
@@ -186,6 +186,20 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// Load config
|
// Load config
|
||||||
let config = AppConfig::load()?;
|
let config = AppConfig::load()?;
|
||||||
|
|
||||||
|
// ── 安全检查:拒绝默认密钥 ──────────────────────────
|
||||||
|
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production" {
|
||||||
|
tracing::error!(
|
||||||
|
"JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET"
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
if config.database.url == "__MUST_SET_VIA_ENV__" {
|
||||||
|
tracing::error!(
|
||||||
|
"数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL"
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize tracing
|
// Initialize tracing
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
|
|||||||
Reference in New Issue
Block a user