use axum::Extension; use axum::Json; use axum::extract::{FromRef, Path, State}; use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; use serde_json::{Value, json}; use uuid::Uuid; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, TenantContext}; use crate::state::AppState; /// POST /api/v1/admin/tenants/:id/rotate-key /// 密钥轮换 — 生成新 DEK,持久化到 tenant_crypto_keys,使缓存失效 pub async fn rotate_tenant_key( State(state): State, Extension(ctx): Extension, Path(tenant_id): Path, ) -> Result>, AppError> where AppState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "tenant.manage")?; // 读取当前最大版本号 let max_version: Option = { let row = state.db.query_one(Statement::from_sql_and_values( DatabaseBackend::Postgres, "SELECT COALESCE(MAX(key_version), 0) as v FROM tenant_crypto_keys WHERE tenant_id = $1 AND deleted_at IS NULL", [tenant_id.into()], )).await.map_err(|e| AppError::Internal(format!("查询密钥版本失败: {}", e)))?; row.and_then(|r| r.try_get_by_index::(0).ok()) }; let current_version = max_version.unwrap_or(0); let new_version = current_version + 1; // 将旧版本标记为不活跃 state.db.execute(Statement::from_sql_and_values( DatabaseBackend::Postgres, "UPDATE tenant_crypto_keys SET is_active = false, updated_at = now() WHERE tenant_id = $1 AND is_active = true AND deleted_at IS NULL", [tenant_id.into()], )).await.map_err(|e| AppError::Internal(format!("停用旧 DEK 失败: {}", e)))?; // 生成新 DEK 并用 KEK 加密 let kek = state.pii_crypto.kek(); let (_new_dek, encrypted_dek) = erp_core::crypto::DekManager::generate_new_dek(kek) .map_err(|e| AppError::Internal(format!("生成新 DEK 失败: {}", e)))?; // 持久化新 DEK let new_id = Uuid::now_v7(); state.db.execute(Statement::from_sql_and_values( DatabaseBackend::Postgres, "INSERT INTO tenant_crypto_keys (id, tenant_id, encrypted_dek, key_version, is_active, created_at, updated_at, version) VALUES ($1, $2, $3, $4, true, now(), now(), 1)", [new_id.into(), tenant_id.into(), encrypted_dek.into(), new_version.into()], )).await.map_err(|e| AppError::Internal(format!("存储新 DEK 失败: {}", e)))?; // 使 DEK 缓存失效 state.pii_crypto.invalidate_dek(tenant_id); tracing::info!( tenant_id = %tenant_id, old_version = current_version, new_version = new_version, "密钥轮换完成(新 DEK 已持久化,缓存已清除)" ); Ok(Json(ApiResponse::ok(json!({ "message": "密钥轮换已完成", "tenant_id": tenant_id, "old_version": current_version, "new_version": new_version, "note": "后台重加密任务需要单独触发,旧数据仍可用旧 DEK 解密" })))) }