quorum_core/db/queries/audit_logs.rs
1//! File to handle logging audit events
2//!
3//! This module provides functionality to log audit events into the database. It includes caching mechanisms
4//! for log types and action types to optimize performance and reduce database queries.
5//!
6//! `log_audit_event` is the only required public function here that is importable to other files.
7
8use crate::db::DB;
9use crate::models::server::AuditEvent;
10use std::collections::HashMap;
11use std::error::Error;
12use std::sync::{Mutex, OnceLock};
13use surrealdb_types::{RecordId, RecordIdKey, SurrealValue};
14
15static LOG_TYPE_CACHE: OnceLock<Mutex<HashMap<String, RecordId>>> = OnceLock::new();
16static ACTION_TYPE_CACHE: OnceLock<Mutex<HashMap<String, RecordId>>> = OnceLock::new();
17
18#[derive(Debug, SurrealValue)]
19struct TypeRecord {
20 id: RecordId,
21}
22
23/// Logs an audit event.
24///
25/// This logs an audit event to the database, which are used to keep a log and record
26/// of things that happen on the server (be it directly in the server or through API calls interacting with the server).
27///
28/// The `action` field in `AuditEvent` uses normalisation, so repeated `action` strings are instead referenced in the `audit_action_type` table,
29/// rather than repeating the same string across multiple audit log entries.
30///
31/// The `target_type_table` and `target_type_table_id` fields are used to reference a specific row in the database by searching the
32/// `target_type_table_id` inside of the `target_type_table` table. Due to how SurrealDB works, the database only needs that `RecordId`
33/// as that includes both the table and id. `target_type_table` is only needed by this function to know what table `target_type_table_id` is pointing towards.
34///
35/// # Arguments
36/// * `db` - A reference to the database connection.
37/// * `event` - An `AuditEvent` struct containing the fields for the audit log entry.
38/// Only `log_type` is required — all other fields default to `None` via `..Default::default()`.
39///
40/// # Returns
41/// * `Ok(())` - If the audit event was successfully logged.
42/// * `Err(Box<dyn Error>)` - If the logging operation failed.
43///
44/// # Examples
45/// ```rust
46/// // Full example with all fields
47/// use models::server::AuditEvent;
48///
49/// let _ = db::queries::audit_logs::log_audit_event(
50/// &db,
51/// AuditEvent {
52/// log_type: "server_event".to_string(),
53/// action: Some("Server startup".to_string()),
54/// target_type_table: Some("server_logs".to_string()),
55/// target_type_table_id: Some("1pen66hlvglaf46q9q8k".to_string()),
56/// new_value: Some("Server started successfully".to_string()),
57/// old_value: Some("Server was not running".to_string()),
58/// user_id: Some("zd0wx5u17prfcw4hn2uf".to_string()),
59/// }
60/// ).await;
61/// ```
62///
63/// ```rust
64/// // Minimal example with only required parameters
65/// use models::server::AuditEvent;
66///
67/// let _ = db::queries::audit_logs::log_audit_event(
68/// &db,
69/// AuditEvent {
70/// log_type: "server_event".to_string(),
71/// user_id: "zd0wx5u17prfcw4hn2uf".to_string(),
72/// ..Default::default()
73/// }
74/// ).await;
75/// ```
76pub async fn log_audit_event(
77 db: &DB,
78 event: AuditEvent,
79) -> Result<(), Box<dyn Error + Send + Sync>> {
80 let log_type_id = get_cached_log_type(db, &event.log_type).await?;
81
82 let action_type_id = match &event.action {
83 Some(a) => Some(get_cached_action_type(db, a).await?),
84 None => None,
85 };
86
87 let user_record_id = event.user_id.map(|id| RecordId {
88 table: "users".into(),
89 key: RecordIdKey::String(id),
90 });
91
92 let target_table_id = match (event.target_type_table, event.target_type_table_id) {
93 (Some(table), Some(id)) => Some(RecordId {
94 table: table.into(),
95 key: RecordIdKey::String(id),
96 }),
97 _ => None,
98 };
99
100 let _response = db
101 .query(
102 "CREATE audit_logs SET
103 log_type_id = $log_type_id,
104 action_type_id = $action_type_id,
105 target_type_table_id = $target_type_table_id,
106 new_value = $new_value,
107 old_value = $old_value,
108 user_id = $user_id",
109 )
110 .bind(("log_type_id", log_type_id))
111 .bind(("action_type_id", action_type_id))
112 .bind(("target_type_table_id", target_table_id))
113 .bind(("new_value", event.new_value))
114 .bind(("old_value", event.old_value))
115 .bind(("user_id", user_record_id))
116 .await?
117 .check()?;
118
119 Ok(())
120}
121
122/// Retrieves the cached log type ID for a given log name, or creates it if it doesn't exist.
123///
124/// This function checks the `LOG_TYPE_CACHE` for the specified `log_name`.
125/// If it exists, it returns the cached `RecordId`. If not, it queries the database to find or create the log type and updates the cache accordingly.
126///
127/// # Arguments
128/// * `db` - A reference to the database connection.
129/// * `log_name` - The name of the log type to retrieve or create.
130///
131/// # Returns
132/// A `RecordId` representing the log type ID associated with the given `log_name`.
133async fn get_cached_log_type(
134 db: &DB,
135 log_name: &str,
136) -> Result<RecordId, Box<dyn Error + Send + Sync>> {
137 let cache = LOG_TYPE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
138
139 if let Some(id) = cache.lock().unwrap().get(log_name).cloned() {
140 return Ok(id);
141 }
142
143 let id = get_or_create_type(db, "log_type", log_name).await?;
144 cache
145 .lock()
146 .unwrap()
147 .insert(log_name.to_string(), id.clone());
148 Ok(id)
149}
150
151/// Retrieves the cached action type ID for a given log name, or creates it if it doesn't exist.
152///
153/// This function checks the `ACTION_TYPE_CACHE` for the specified `action_name`.
154/// If it exists, it returns the cached `RecordId`. If not, it queries the database to find or create the action type and updates the cache accordingly.
155///
156/// # Arguments
157/// * `db` - A reference to the database connection.
158/// * `action_name` - The name of the action type to retrieve or create.
159///
160/// # Returns
161/// A `RecordId` representing the action type ID associated with the given `action_name`.
162async fn get_cached_action_type(
163 db: &DB,
164 action_name: &str,
165) -> Result<RecordId, Box<dyn Error + Send + Sync>> {
166 let cache = ACTION_TYPE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
167
168 if let Some(id) = cache.lock().unwrap().get(action_name).cloned() {
169 return Ok(id);
170 }
171
172 let id = get_or_create_type(db, "audit_action_type", action_name).await?;
173 cache
174 .lock()
175 .unwrap()
176 .insert(action_name.to_string(), id.clone());
177 Ok(id)
178}
179
180/// Retrieves or creates a type record in the specified table based on the given name.
181///
182/// This function checks if a record with the specified name exists in the given table.
183/// If it exists, it returns the `RecordId`. If not, it creates a new record with that name and returns the new `RecordId`.
184///
185/// # Arguments
186/// * `db` - A reference to the database connection.
187/// * `table` - The name of the table to query or insert into (e.g., "log_type" or "audit_action_type").
188/// * `name` - The name of the type to retrieve or create.
189///
190/// # Returns
191/// A `RecordId` representing the ID of the existing or newly created type record.
192async fn get_or_create_type(
193 db: &DB,
194 table: &str,
195 name: &str,
196) -> Result<RecordId, Box<dyn Error + Send + Sync>> {
197 let query = format!("SELECT id FROM {} WHERE name = $name LIMIT 1", table);
198 let response = db.query(&query).bind(("name", name)).await?.check()?;
199
200 let mut response = response;
201 let result: Option<TypeRecord> = response.take(0)?;
202
203 if let Some(record) = result {
204 return Ok(record.id);
205 }
206
207 let create_query = format!("CREATE {} SET name = $name RETURN id", table);
208 let create_response = db
209 .query(&create_query)
210 .bind(("name", name))
211 .await?
212 .check()?;
213
214 let mut create_response = create_response;
215 let created: Option<TypeRecord> = create_response.take(0)?;
216 Ok(created.ok_or("Failed to create type record")?.id)
217}