feat(core): 审计日志哈希链 — prev_hash + record_hash + 完整性验证
- 迁移 087: audit_logs 表添加 prev_hash/record_hash 列 + 索引 - audit_service::record() 写入时查询前一条 record_hash 作为 prev_hash - SHA256(id+action+resource_type+resource_id+created_at+prev_hash) 计算 record_hash - verify_hash_chain() 验证链完整性,返回 (总记录数, 断链数)
This commit is contained in:
@@ -86,6 +86,7 @@ mod m20260427_000083_create_follow_up_template;
|
||||
mod m20260427_000084_domain_events_cleanup;
|
||||
mod m20260427_000085_processed_events;
|
||||
mod m20260427_000086_enable_rls_all_tables;
|
||||
mod m20260427_000087_audit_logs_hash_chain;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -179,6 +180,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260427_000084_domain_events_cleanup::Migration),
|
||||
Box::new(m20260427_000085_processed_events::Migration),
|
||||
Box::new(m20260427_000086_enable_rls_all_tables::Migration),
|
||||
Box::new(m20260427_000087_audit_logs_hash_chain::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let conn = manager.get_connection();
|
||||
|
||||
conn.execute_unprepared(
|
||||
"ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS prev_hash TEXT",
|
||||
)
|
||||
.await?;
|
||||
|
||||
conn.execute_unprepared(
|
||||
"ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS record_hash TEXT",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 为 record_hash 创建索引(用于快速查找最新哈希)
|
||||
conn.execute_unprepared(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_record_hash
|
||||
ON audit_logs (record_hash) WHERE record_hash IS NOT NULL",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 按 tenant_id + created_at DESC 查找最新哈希的索引
|
||||
conn.execute_unprepared(
|
||||
"CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant_created
|
||||
ON audit_logs (tenant_id, created_at DESC)",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let conn = manager.get_connection();
|
||||
|
||||
conn.execute_unprepared(
|
||||
"DROP INDEX IF EXISTS idx_audit_logs_tenant_created",
|
||||
)
|
||||
.await?;
|
||||
|
||||
conn.execute_unprepared(
|
||||
"DROP INDEX IF EXISTS idx_audit_logs_record_hash",
|
||||
)
|
||||
.await?;
|
||||
|
||||
conn.execute_unprepared(
|
||||
"ALTER TABLE audit_logs DROP COLUMN IF EXISTS record_hash",
|
||||
)
|
||||
.await?;
|
||||
|
||||
conn.execute_unprepared(
|
||||
"ALTER TABLE audit_logs DROP COLUMN IF EXISTS prev_hash",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user