1use anyhow::Result;
7use chrono::Utc;
8use rusqlite::Connection;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AuditRow {
14 pub ts: String,
15 pub actor: Option<String>,
16 pub action: String,
17 pub target: Option<String>,
18 pub key_fingerprint: Option<String>,
19 pub metadata: Value,
20}
21
22pub fn log(
24 conn: &Connection,
25 actor: Option<&str>,
26 action: &str,
27 target: Option<&str>,
28 key_fingerprint: Option<&str>,
29 metadata: &Value,
30) -> Result<()> {
31 let ts = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
32 let metadata_json = serde_json::to_string(metadata)?;
33 conn.execute(
34 "INSERT INTO audit_log (ts, actor, action, target, key_fingerprint, metadata_json)
35 VALUES (?,?,?,?,?,?)",
36 rusqlite::params![ts, actor, action, target, key_fingerprint, metadata_json],
37 )?;
38 Ok(())
39}
40
41pub fn tail(conn: &Connection, n: i64) -> Result<Vec<AuditRow>> {
43 let mut stmt = conn.prepare(
44 "SELECT ts, actor, action, target, key_fingerprint, metadata_json
45 FROM audit_log ORDER BY id DESC LIMIT ?",
46 )?;
47 let rows = stmt
48 .query_map([n], |r| {
49 let metadata_json: Option<String> = r.get(5)?;
50 Ok(AuditRow {
51 ts: r.get(0)?,
52 actor: r.get(1)?,
53 action: r.get(2)?,
54 target: r.get(3)?,
55 key_fingerprint: r.get(4)?,
56 metadata: serde_json::from_str(metadata_json.as_deref().unwrap_or("{}"))
57 .unwrap_or(Value::Null),
58 })
59 })?
60 .collect::<rusqlite::Result<Vec<_>>>()?;
61 Ok(rows)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 fn fresh() -> Connection {
69 let c = crate::db::connect_in_memory().unwrap();
70 crate::db::bootstrap(&c).unwrap();
71 c
72 }
73
74 #[test]
75 fn write_then_tail() {
76 let c = fresh();
77 log(
78 &c,
79 Some("alice"),
80 "login",
81 Some("/v1/auth/login"),
82 Some("SHA256:fp"),
83 &serde_json::json!({"ok": true}),
84 )
85 .unwrap();
86 log(
87 &c,
88 None,
89 "signed_request",
90 Some("/v1/workers"),
91 None,
92 &serde_json::json!({"reason": "missing_headers"}),
93 )
94 .unwrap();
95 let rows = tail(&c, 5).unwrap();
96 assert_eq!(rows.len(), 2);
97 assert_eq!(rows[0].action, "signed_request");
99 assert_eq!(rows[1].action, "login");
100 assert_eq!(rows[1].actor.as_deref(), Some("alice"));
101 }
102}