use std::sync::Arc;
use nodedb_types::AuditDmlMode;
use crate::control::state::SharedState;
use crate::event::types::{EventSource, WriteEvent, WriteOp};
pub fn audit_dml_event(event: &WriteEvent, state: &Arc<SharedState>) {
match event.source {
EventSource::User => {}
EventSource::Trigger
| EventSource::RaftFollower
| EventSource::CrdtSync
| EventSource::Deferred => return,
}
match event.op {
WriteOp::Insert
| WriteOp::Update
| WriteOp::Delete
| WriteOp::BulkInsert { .. }
| WriteOp::BulkDelete { .. } => {}
WriteOp::Heartbeat => return,
}
let db_id = match state
.collection_to_database
.lookup(event.tenant_id, &event.collection)
{
Some(id) => id,
None => return, };
let mode = state.audit_dml_cache.get(db_id);
match mode {
AuditDmlMode::None => return,
AuditDmlMode::Writes | AuditDmlMode::All => {}
}
let detail = format!(
"{} {}:{} lsn={}",
event.op,
event.collection,
event.row_id,
event.lsn.as_u64(),
);
let source = event.user_id.as_deref().unwrap_or("unknown").to_string();
state.audit_record_with_db(
crate::control::security::audit::AuditEvent::DmlAudit,
Some(event.tenant_id),
Some(db_id),
&source,
&detail,
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::control::state::audit_dml_cache::AuditDmlCache;
use crate::control::state::collection_to_database::CollectionToDatabase;
use crate::event::types::{RowId, WriteOp};
use crate::types::{Lsn, TenantId, VShardId};
use nodedb_types::DatabaseId;
fn minimal_event(source: EventSource, op: WriteOp) -> WriteEvent {
WriteEvent {
sequence: 1,
collection: Arc::from("orders"),
op,
row_id: RowId::new("o-1"),
lsn: Lsn::new(100),
tenant_id: TenantId::new(1),
vshard_id: VShardId::new(0),
source,
new_value: Some(Arc::from(b"data".as_slice())),
old_value: None,
system_time_ms: None,
valid_time_ms: None,
user_id: Some(Arc::from("alice")),
statement_digest: None,
}
}
#[test]
fn skips_trigger_source() {
let cache = AuditDmlCache::new();
let coll_db = CollectionToDatabase::new();
let db_id = DatabaseId::new(42);
coll_db.insert(TenantId::new(1), Arc::from("orders"), db_id);
cache.set(db_id, AuditDmlMode::Writes);
let event = minimal_event(EventSource::Trigger, WriteOp::Insert);
match event.source {
EventSource::User => panic!("should not be User"),
EventSource::Trigger
| EventSource::RaftFollower
| EventSource::CrdtSync
| EventSource::Deferred => {}
}
}
#[test]
fn skips_heartbeat_op() {
let event = minimal_event(EventSource::User, WriteOp::Heartbeat);
match event.op {
WriteOp::Heartbeat => {} WriteOp::Insert
| WriteOp::Update
| WriteOp::Delete
| WriteOp::BulkInsert { .. }
| WriteOp::BulkDelete { .. } => panic!("should not be data op"),
}
}
#[test]
fn none_mode_skips() {
let cache = AuditDmlCache::new();
let db_id = DatabaseId::new(42);
cache.set(db_id, AuditDmlMode::None);
assert_eq!(cache.get(db_id), AuditDmlMode::None);
}
#[test]
fn writes_mode_triggers() {
let cache = AuditDmlCache::new();
let db_id = DatabaseId::new(42);
cache.set(db_id, AuditDmlMode::Writes);
assert_ne!(cache.get(db_id), AuditDmlMode::None);
}
#[test]
fn unknown_collection_skips() {
let coll_db = CollectionToDatabase::new();
let result = coll_db.lookup(TenantId::new(1), "unknown_collection");
assert!(result.is_none());
}
}