use crate::audit::AuditLog; use crate::entity::audit_log; use crate::request_info::RequestInfo; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set}; use sha2::{Digest, Sha256}; use tracing; /// 持久化审计日志到 audit_logs 表。 /// /// 使用 fire-and-forget 模式:失败仅记录 warning 日志,不影响业务操作。 /// /// 自动从 task_local 读取当前请求的 IP 和 User-Agent, /// 如果 AuditLog 中已有 ip_address/user_agent 则不覆盖。 /// /// 哈希链:查询同租户最新一条记录的 record_hash 作为 prev_hash, /// 计算 SHA256(id + action + resource_type + resource_id + created_at + prev_hash) 作为 record_hash。 pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) { // 自动填充请求来源信息(仅当调用方未显式设置时) if let Some(info) = RequestInfo::try_current() { if log.ip_address.is_none() { log.ip_address = info.ip_address; } if log.user_agent.is_none() { log.user_agent = info.user_agent; } } // 查询同租户最新一条记录的 record_hash 作为 prev_hash let prev_hash = audit_log::Entity::find() .filter(audit_log::Column::TenantId.eq(log.tenant_id)) .filter(audit_log::Column::RecordHash.is_not_null()) .order_by_desc(audit_log::Column::CreatedAt) .one(db) .await .ok() .flatten() .and_then(|m| m.record_hash); // 计算当前记录的 record_hash let record_hash = compute_record_hash(&log, prev_hash.as_deref()); let model = audit_log::ActiveModel { id: Set(log.id), tenant_id: Set(log.tenant_id), user_id: Set(log.user_id), action: Set(log.action), resource_type: Set(log.resource_type), resource_id: Set(log.resource_id), old_value: Set(log.old_value), new_value: Set(log.new_value), ip_address: Set(log.ip_address), user_agent: Set(log.user_agent), created_at: Set(log.created_at), prev_hash: Set(prev_hash), record_hash: Set(Some(record_hash)), }; if let Err(e) = model.insert(db).await { tracing::warn!(error = %e, "审计日志写入失败"); } } /// 计算 record_hash: SHA256(id + action + resource_type + resource_id + created_at + prev_hash) fn compute_record_hash(log: &AuditLog, prev_hash: Option<&str>) -> String { let mut hasher = Sha256::new(); hasher.update(log.id.to_string().as_bytes()); hasher.update(log.action.as_bytes()); hasher.update(log.resource_type.as_bytes()); hasher.update( log.resource_id .map(|id| id.to_string()) .unwrap_or_default() .as_bytes(), ); hasher.update(log.created_at.to_rfc3339().as_bytes()); hasher.update(prev_hash.unwrap_or("").as_bytes()); format!("{:x}", hasher.finalize()) } /// 验证审计日志哈希链完整性。 /// /// 检查指定租户的所有含 record_hash 的日志记录, /// 验证每条记录的 prev_hash 是否等于前一条的 record_hash, /// 以及 record_hash 是否可以重新计算验证。 /// /// 返回 (总记录数, 断链数)。 pub async fn verify_hash_chain( db: &sea_orm::DatabaseConnection, tenant_id: uuid::Uuid, ) -> Result<(usize, usize), sea_orm::DbErr> { use sea_orm::QueryOrder; let records = audit_log::Entity::find() .filter(audit_log::Column::TenantId.eq(tenant_id)) .filter(audit_log::Column::RecordHash.is_not_null()) .order_by_asc(audit_log::Column::CreatedAt) .all(db) .await?; let total = records.len(); let mut broken = 0; let mut prev: Option = None; for record in &records { // 验证 prev_hash 指向正确 if prev.as_deref() != record.prev_hash.as_deref() { broken += 1; } // 验证 record_hash 可重算 let log = AuditLog { id: record.id, tenant_id: record.tenant_id, user_id: record.user_id, action: record.action.clone(), resource_type: record.resource_type.clone(), resource_id: record.resource_id, old_value: record.old_value.clone(), new_value: record.new_value.clone(), ip_address: record.ip_address.clone(), user_agent: record.user_agent.clone(), created_at: record.created_at, }; let expected = compute_record_hash(&log, record.prev_hash.as_deref()); if Some(expected.as_str()) != record.record_hash.as_deref() { broken += 1; } prev = record.record_hash.clone(); } Ok((total, broken)) }