Skip to main content

confium_tc/coordinator/
audit.rs

1//! Audit log for coordinator sessions.
2
3use crate::coordinator::session::{SessionId, SignerId};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7/// A single audit log entry.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AuditEntry {
10    /// When the event occurred.
11    pub timestamp: DateTime<Utc>,
12    /// Type of event.
13    pub event: AuditEvent,
14    /// Session involved.
15    pub session_id: SessionId,
16}
17
18/// Type of audit event.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum AuditEvent {
22    /// Session created.
23    SessionCreated {
24        /// Requesting actor.
25        requested_by: SignerId,
26        /// Quorum.
27        quorum_id: String,
28    },
29    /// Commitment received from signer.
30    CommitmentReceived {
31        /// Signer.
32        signer: SignerId,
33    },
34    /// Share received from signer.
35    ShareReceived {
36        /// Signer.
37        signer: SignerId,
38    },
39    /// Aggregation completed.
40    Aggregated,
41    /// Session expired.
42    Expired,
43    /// Session aborted.
44    Aborted {
45        /// Reason.
46        reason: String,
47    },
48}
49
50/// Append-only audit log.
51#[derive(Debug, Default)]
52pub struct AuditLog {
53    entries: Vec<AuditEntry>,
54}
55
56impl AuditLog {
57    /// Construct a new empty log.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Append an entry.
63    pub fn append(&mut self, session_id: impl Into<SessionId>, event: AuditEvent) {
64        self.entries.push(AuditEntry {
65            timestamp: Utc::now(),
66            event,
67            session_id: session_id.into(),
68        });
69    }
70
71    /// Get all entries for a session.
72    pub fn entries_for(&self, session_id: &str) -> Vec<&AuditEntry> {
73        self.entries
74            .iter()
75            .filter(|e| e.session_id == session_id)
76            .collect()
77    }
78
79    /// All entries.
80    pub fn all(&self) -> &[AuditEntry] {
81        &self.entries
82    }
83
84    /// Serialize to JSONL.
85    pub fn to_jsonl(&self) -> Result<String, serde_json::Error> {
86        let mut out = String::new();
87        for entry in &self.entries {
88            out.push_str(&serde_json::to_string(entry)?);
89            out.push('\n');
90        }
91        Ok(out)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn append_and_query() {
101        let mut log = AuditLog::new();
102        log.append(
103            "session-1",
104            AuditEvent::SessionCreated {
105                requested_by: "alice".into(),
106                quorum_id: "biml-root".into(),
107            },
108        );
109        log.append(
110            "session-1",
111            AuditEvent::CommitmentReceived {
112                signer: "alice".into(),
113            },
114        );
115        log.append(
116            "session-2",
117            AuditEvent::SessionCreated {
118                requested_by: "bob".into(),
119                quorum_id: "biml-root".into(),
120            },
121        );
122
123        let s1_entries = log.entries_for("session-1");
124        assert_eq!(s1_entries.len(), 2);
125        let s2_entries = log.entries_for("session-2");
126        assert_eq!(s2_entries.len(), 1);
127    }
128
129    #[test]
130    fn jsonl_round_trips() {
131        let mut log = AuditLog::new();
132        log.append("session-1", AuditEvent::Aggregated);
133        let jsonl = log.to_jsonl().unwrap();
134        assert!(jsonl.contains("session-1"));
135        assert!(jsonl.contains("aggregated"));
136    }
137}