1use crate::access_policy::{Denial, ResourceId};
15use std::collections::VecDeque;
16use std::fmt;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum Decision {
21 Allowed,
23 Denied(Denial),
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum OpStatus {
30 Ok,
32 Rejected,
34 BackendFault,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct AuditRecord {
43 pub session: Option<u64>,
45 pub resource: Option<ResourceId>,
47 pub operation: &'static str,
50 pub offset: Option<u64>,
52 pub decision: Decision,
54 pub status: OpStatus,
56 pub value: Option<u32>,
58 pub timestamp_ns: i64,
61 pub run_id: Option<String>,
64 pub item_index: Option<u32>,
68}
69
70impl AuditRecord {
71 pub fn lifecycle(operation: &'static str, timestamp_ns: i64) -> Self {
73 Self {
74 session: None,
75 resource: None,
76 operation,
77 offset: None,
78 decision: Decision::Allowed,
79 status: OpStatus::Ok,
80 value: None,
81 timestamp_ns,
82 run_id: None,
83 item_index: None,
84 }
85 }
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct AuditEntry {
91 pub seq: u64,
93 pub record: AuditRecord,
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct AuditPage {
100 pub entries: Vec<AuditEntry>,
102 pub oldest_retained: Option<u64>,
106 pub next_cursor: u64,
108}
109
110pub type AuditObserver = Box<dyn Fn(u64, &AuditRecord) + Send + Sync>;
119
120pub struct AuditRing {
122 capacity: usize,
123 next_seq: u64,
124 entries: VecDeque<AuditEntry>,
125 observer: Option<AuditObserver>,
126}
127
128impl fmt::Debug for AuditRing {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.debug_struct("AuditRing")
131 .field("capacity", &self.capacity)
132 .field("next_seq", &self.next_seq)
133 .field("entries", &self.entries)
134 .field("has_observer", &self.observer.is_some())
135 .finish()
136 }
137}
138
139impl AuditRing {
140 pub fn new(capacity: usize) -> Self {
146 assert!(capacity > 0, "audit ring capacity must be nonzero");
147 Self { capacity, next_seq: 0, entries: VecDeque::with_capacity(capacity), observer: None }
148 }
149
150 pub fn set_observer(&mut self, observer: AuditObserver) {
154 self.observer = Some(observer);
155 }
156
157 pub fn has_observer(&self) -> bool {
159 self.observer.is_some()
160 }
161
162 pub fn append(&mut self, record: AuditRecord) -> u64 {
165 let seq = self.next_seq;
166 self.next_seq += 1;
167 if let Some(observer) = &self.observer {
168 observer(seq, &record);
169 }
170 if self.entries.len() == self.capacity {
171 self.entries.pop_front();
172 }
173 self.entries.push_back(AuditEntry { seq, record });
174 seq
175 }
176
177 pub fn oldest_retained(&self) -> Option<u64> {
179 self.entries.front().map(|entry| entry.seq)
180 }
181
182 pub fn next_sequence(&self) -> u64 {
184 self.next_seq
185 }
186
187 pub fn capacity(&self) -> usize {
189 self.capacity
190 }
191
192 pub fn read(&self, cursor: u64, limit: usize) -> AuditPage {
194 let entries: Vec<AuditEntry> =
195 self.entries.iter().filter(|entry| entry.seq >= cursor).take(limit).cloned().collect();
196 let next_cursor = entries.last().map_or(cursor, |entry| entry.seq + 1);
197 AuditPage { entries, oldest_retained: self.oldest_retained(), next_cursor }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn record(op: &'static str) -> AuditRecord {
206 AuditRecord::lifecycle(op, 0)
207 }
208
209 #[test]
210 fn sequences_are_monotonic() {
211 let mut ring = AuditRing::new(4);
212 for expected in 0..10 {
213 assert_eq!(ring.append(record("op")), expected);
214 }
215 assert_eq!(ring.next_sequence(), 10);
216 }
217
218 #[test]
219 fn wrap_discards_oldest_but_keeps_sequence() {
220 let mut ring = AuditRing::new(3);
221 for _ in 0..5 {
222 ring.append(record("op"));
223 }
224 assert_eq!(ring.oldest_retained(), Some(2));
226 let page = ring.read(0, 10);
227 let seqs: Vec<u64> = page.entries.iter().map(|entry| entry.seq).collect();
228 assert_eq!(seqs, vec![2, 3, 4]);
229 assert_eq!(page.oldest_retained, Some(2));
231 }
232
233 #[test]
234 fn pagination_via_next_cursor() {
235 let mut ring = AuditRing::new(10);
236 for _ in 0..6 {
237 ring.append(record("op"));
238 }
239 let first = ring.read(0, 4);
240 assert_eq!(first.entries.len(), 4);
241 assert_eq!(first.next_cursor, 4);
242 let second = ring.read(first.next_cursor, 4);
243 let seqs: Vec<u64> = second.entries.iter().map(|entry| entry.seq).collect();
244 assert_eq!(seqs, vec![4, 5]);
245 assert_eq!(second.next_cursor, 6);
246 let done = ring.read(second.next_cursor, 4);
248 assert!(done.entries.is_empty());
249 assert_eq!(done.next_cursor, 6);
250 }
251
252 #[test]
253 fn empty_ring_reads_empty() {
254 let ring = AuditRing::new(4);
255 let page = ring.read(0, 4);
256 assert!(page.entries.is_empty());
257 assert_eq!(page.oldest_retained, None);
258 assert_eq!(page.next_cursor, 0);
259 }
260
261 #[test]
262 fn observer_sees_every_append_with_its_sequence() {
263 use std::sync::{Arc, Mutex};
264
265 let seen = Arc::new(Mutex::new(Vec::new()));
266 let mut ring = AuditRing::new(2);
267 ring.append(record("before_observer"));
268 let seen_by_observer = Arc::clone(&seen);
269 ring.set_observer(Box::new(move |seq, record| {
270 seen_by_observer.lock().unwrap().push((seq, record.operation));
271 }));
272 for op in ["a", "b", "c"] {
273 ring.append(record(op));
274 }
275 assert_eq!(*seen.lock().unwrap(), vec![(1, "a"), (2, "b"), (3, "c")]);
278 let page = ring.read(0, 10);
279 let ops: Vec<&str> = page.entries.iter().map(|entry| entry.record.operation).collect();
280 assert_eq!(ops, vec!["b", "c"]);
281 }
282
283 #[test]
284 fn debug_reports_observer_presence() {
285 let mut ring = AuditRing::new(1);
286 assert!(!ring.has_observer());
287 ring.set_observer(Box::new(|_, _| {}));
288 assert!(ring.has_observer());
289 assert!(format!("{ring:?}").contains("has_observer: true"));
290 }
291
292 #[test]
293 #[should_panic(expected = "capacity must be nonzero")]
294 fn zero_capacity_panics() {
295 let _ = AuditRing::new(0);
296 }
297}