1use std::io::{self, Write};
6
7use rusqlite::{Connection, Transaction, params};
8use serde_json::json;
9use time::OffsetDateTime;
10use time::format_description::well_known::Rfc3339;
11
12use crate::ids::{KeyId, VersionId};
13use crate::query::{self, BadQuery};
14
15pub const SCHEMA: &str = "
17CREATE TABLE audit (
18 seq INTEGER PRIMARY KEY AUTOINCREMENT,
19 at TEXT NOT NULL,
20 actor TEXT NOT NULL,
21 action TEXT NOT NULL,
22 target TEXT,
23 version TEXT,
24 status INTEGER NOT NULL,
25 count INTEGER NOT NULL
26);
27CREATE INDEX audit_counted ON audit (actor, action, status, at);
28";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Actor {
33 Key(KeyId),
35 Sweep,
37 Cli,
39 Unparsed,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Action {
47 Create,
48 Replace,
49 Read,
50 Delete,
51 Purge,
52 LegalRead,
53 LegalVersions,
54 Restore,
55 Hold,
56 Release,
57 KeyCreate,
58 KeyRevoke,
59 KeyList,
60 KeyInfo,
61 Retention,
62 Health,
63 AuthFailed,
64 TenantCreate,
65 TenantList,
66 TenantRetention,
67 TenantRemove,
68 RekeyMaster,
69 RekeyTenant,
70 List,
71 AuditRead,
72 Openapi,
73}
74
75impl Action {
76 pub fn as_str(self) -> &'static str {
78 match self {
79 Action::Create => "create",
80 Action::Replace => "replace",
81 Action::Read => "read",
82 Action::Delete => "delete",
83 Action::Purge => "purge",
84 Action::LegalRead => "legal_read",
85 Action::LegalVersions => "legal_versions",
86 Action::Restore => "restore",
87 Action::Hold => "hold",
88 Action::Release => "release",
89 Action::KeyCreate => "key_create",
90 Action::KeyRevoke => "key_revoke",
91 Action::KeyList => "key_list",
92 Action::KeyInfo => "key_info",
93 Action::Retention => "retention",
94 Action::Health => "health",
95 Action::AuthFailed => "auth_failed",
96 Action::TenantCreate => "tenant_create",
97 Action::TenantList => "tenant_list",
98 Action::TenantRetention => "tenant_retention",
99 Action::TenantRemove => "tenant_remove",
100 Action::RekeyMaster => "rekey_master",
101 Action::RekeyTenant => "rekey_tenant",
102 Action::List => "list",
103 Action::AuditRead => "audit_read",
104 Action::Openapi => "openapi",
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct Entry {
112 pub actor: Actor,
113 pub action: Action,
114 pub target: Option<String>,
116 pub version: Option<VersionId>,
118 pub status: u16,
120}
121
122#[derive(Debug)]
124pub enum AuditError {
125 Sqlite(rusqlite::Error),
126 Write(io::Error),
127}
128
129impl From<rusqlite::Error> for AuditError {
130 fn from(e: rusqlite::Error) -> AuditError {
131 AuditError::Sqlite(e)
132 }
133}
134
135impl Actor {
136 fn name(self) -> String {
138 match self {
139 Actor::Key(id) => id.to_string(),
140 Actor::Sweep => "sweep".to_owned(),
141 Actor::Cli => "cli".to_owned(),
142 Actor::Unparsed => String::new(),
143 }
144 }
145}
146
147fn rfc3339(now: OffsetDateTime) -> String {
149 now.to_offset(time::UtcOffset::UTC)
150 .format(&Rfc3339)
151 .expect("every UTC time from the clock formats")
152}
153
154pub fn record(tx: &Transaction, entry: &Entry, now: OffsetDateTime) -> Result<(), AuditError> {
156 tx.execute(
157 "INSERT INTO audit (at, actor, action, target, version, status, count)
158 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1)",
159 params![
160 rfc3339(now),
161 entry.actor.name(),
162 entry.action.as_str(),
163 entry.target,
164 entry.version.map(|v| v.to_string()),
165 entry.status,
166 ],
167 )?;
168 Ok(())
169}
170
171pub fn count(tx: &Transaction, entry: &Entry, now: OffsetDateTime) -> Result<(), AuditError> {
175 let minute = now
176 .to_offset(time::UtcOffset::UTC)
177 .replace_second(0)
178 .and_then(|t| t.replace_nanosecond(0))
179 .expect("0 is a valid second and nanosecond");
180 let fields = params![
181 rfc3339(minute),
182 entry.actor.name(),
183 entry.action.as_str(),
184 entry.target,
185 entry.version.map(|v| v.to_string()),
186 entry.status,
187 ];
188 let added = tx.execute(
189 "UPDATE audit SET count = count + 1
190 WHERE at = ?1 AND actor = ?2 AND action = ?3 AND target IS ?4 AND version IS ?5
191 AND status = ?6",
192 fields,
193 )?;
194 if added == 0 {
195 tx.execute(
196 "INSERT INTO audit (at, actor, action, target, version, status, count)
197 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1)",
198 fields,
199 )?;
200 }
201 Ok(())
202}
203
204pub fn export(log: &Connection, since: Option<i64>, out: &mut dyn Write) -> Result<(), AuditError> {
206 lines(log, since.unwrap_or(0), -1, out)
208}
209
210fn lines(log: &Connection, after: i64, limit: i64, out: &mut dyn Write) -> Result<(), AuditError> {
213 let mut rows = log.prepare(
214 "SELECT seq, at, actor, action, target, version, status, count
215 FROM audit WHERE seq > ?1 ORDER BY seq LIMIT ?2",
216 )?;
217 let mut rows = rows.query(params![after, limit])?;
218 while let Some(row) = rows.next()? {
219 let record = json!({
220 "seq": row.get::<_, i64>(0)?,
221 "at": row.get::<_, String>(1)?,
222 "actor": row.get::<_, String>(2)?,
223 "action": row.get::<_, String>(3)?,
224 "target": row.get::<_, Option<String>>(4)?,
225 "version": row.get::<_, Option<String>>(5)?,
226 "status": row.get::<_, u16>(6)?,
227 "count": row.get::<_, i64>(7)?,
228 });
229 writeln!(out, "{record}").map_err(AuditError::Write)?;
230 }
231 Ok(())
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub struct AuditQuery {
238 pub after: i64,
239 pub limit: u32,
240}
241
242impl AuditQuery {
243 pub const DEFAULT_LIMIT: u32 = 1000;
244 pub const MAX_LIMIT: u32 = 10000;
245
246 pub fn new(after: Option<u64>, limit: Option<u64>) -> Result<AuditQuery, BadQuery> {
250 let after = after
251 .map(|n| i64::try_from(n).map_err(|_| BadQuery))
252 .transpose()?
253 .unwrap_or(0);
254 let limit = match limit {
255 None => AuditQuery::DEFAULT_LIMIT,
256 Some(n) => u32::try_from(n)
257 .ok()
258 .filter(|n| (1..=AuditQuery::MAX_LIMIT).contains(n))
259 .ok_or(BadQuery)?,
260 };
261 Ok(AuditQuery { after, limit })
262 }
263
264 pub fn from_query(text: Option<&str>) -> Result<AuditQuery, BadQuery> {
267 let read = query::pairs(text, &["after", "limit"])?;
268 let number = |name| {
269 read.get(name)
270 .map(|n: &String| n.parse::<u64>().map_err(|_| BadQuery))
271 .transpose()
272 };
273 AuditQuery::new(number("after")?, number("limit")?)
274 }
275}
276
277pub fn page(log: &Connection, query: &AuditQuery, out: &mut dyn Write) -> Result<(), AuditError> {
280 lines(log, query.after, i64::from(query.limit), out)
281}
282
283#[cfg(test)]
285pub(crate) mod testing {
286 use rusqlite::Connection;
287 use serde_json::Value;
288
289 pub(crate) fn exported(log: &Connection, since: Option<i64>) -> Vec<Value> {
291 let mut out = Vec::new();
292 super::export(log, since, &mut out).expect("export");
293 String::from_utf8(out)
294 .expect("UTF-8")
295 .lines()
296 .map(|line| serde_json::from_str(line).expect("one JSON object per line"))
297 .collect()
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use serde_json::{Value, json};
304 use time::macros::datetime;
305
306 use super::testing::exported;
307 use super::*;
308 use crate::catalog;
309 use crate::envelope::Key;
310
311 struct Log {
312 _dir: tempfile::TempDir,
313 conn: Connection,
314 }
315
316 fn log() -> Log {
317 let dir = tempfile::tempdir().expect("temp dir");
318 let key = Key::generate().expect("key");
319 let conn = catalog::open(&dir.path().join("catalog.sqlite"), &key, SCHEMA).expect("opens");
320 Log { _dir: dir, conn }
321 }
322
323 fn read_by(key: KeyId, status: u16) -> Entry {
324 Entry {
325 actor: Actor::Key(key),
326 action: Action::Read,
327 target: Some("01a07da8-c2bb-83e5-b9a2-279e0d16bc46".into()),
328 version: Some(VersionId([0x22; 16])),
329 status,
330 }
331 }
332
333 #[test]
334 fn records_are_written_and_exported_back() {
335 let mut log = log();
336 let key = Key::generate().expect("key").id();
337 let tx = log.conn.transaction().expect("tx");
338 record(
339 &tx,
340 &read_by(key, 200),
341 datetime!(2026-10-23 12:34:56.789 UTC),
342 )
343 .expect("record");
344 let purge = Entry {
345 actor: Actor::Sweep,
346 action: Action::Purge,
347 target: Some("01a07da8-c2bb-83e5-b9a2-279e0d16bc46".into()),
348 version: None,
349 status: 200,
350 };
351 record(&tx, &purge, datetime!(2026-10-23 12:35:00 UTC)).expect("record");
352 tx.commit().expect("commit");
353 assert_eq!(
354 exported(&log.conn, None),
355 [
356 json!({"seq": 1, "at": "2026-10-23T12:34:56.789Z", "actor": key.to_string(),
357 "action": "read", "target": "01a07da8-c2bb-83e5-b9a2-279e0d16bc46",
358 "version": "22".repeat(16), "status": 200, "count": 1}),
359 json!({"seq": 2, "at": "2026-10-23T12:35:00Z", "actor": "sweep",
360 "action": "purge", "target": "01a07da8-c2bb-83e5-b9a2-279e0d16bc46",
361 "version": null, "status": 200, "count": 1}),
362 ]
363 );
364 assert_eq!(
365 exported(&log.conn, Some(1)).len(),
366 1,
367 "only records after seq 1"
368 );
369 assert_eq!(exported(&log.conn, Some(2)).len(), 0);
370 }
371
372 #[test]
373 fn times_are_written_in_utc() {
374 let mut log = log();
375 let key = Key::generate().expect("key").id();
376 let tx = log.conn.transaction().expect("tx");
377 record(
378 &tx,
379 &read_by(key, 200),
380 datetime!(2026-10-23 14:34:56 +02:00),
381 )
382 .expect("record");
383 tx.commit().expect("commit");
384 assert_eq!(exported(&log.conn, None)[0]["at"], "2026-10-23T12:34:56Z");
385 }
386
387 #[test]
388 fn counted_records_hold_one_row_per_actor_action_status_and_minute() {
389 let mut log = log();
390 let health = |status| Entry {
391 actor: Actor::Unparsed,
392 action: Action::Health,
393 target: None,
394 version: None,
395 status,
396 };
397 let tx = log.conn.transaction().expect("tx");
398 for second in [0, 30, 59] {
399 let now = datetime!(2026-10-23 12:34:00 UTC) + time::Duration::seconds(second);
400 count(&tx, &health(200), now).expect("count");
401 }
402 count(&tx, &health(200), datetime!(2026-10-23 12:35:00 UTC)).expect("next minute");
403 count(&tx, &health(503), datetime!(2026-10-23 12:34:10 UTC)).expect("other status");
404 let failed = Entry {
407 action: Action::AuthFailed,
408 ..health(503)
409 };
410 count(&tx, &failed, datetime!(2026-10-23 12:34:20 UTC)).expect("other action");
411 let cli = Entry {
412 actor: Actor::Cli,
413 ..health(200)
414 };
415 count(&tx, &cli, datetime!(2026-10-23 12:34:40 UTC)).expect("other actor");
416 tx.commit().expect("commit");
417 let rows: Vec<(String, String, String, u64, u64)> = exported(&log.conn, None)
418 .iter()
419 .map(|r| {
420 (
421 r["at"].as_str().expect("at").to_owned(),
422 r["actor"].as_str().expect("actor").to_owned(),
423 r["action"].as_str().expect("action").to_owned(),
424 r["status"].as_u64().expect("status"),
425 r["count"].as_u64().expect("count"),
426 )
427 })
428 .collect();
429 let row = |at: &str, actor: &str, action: &str, status, count| {
430 (
431 at.to_owned(),
432 actor.to_owned(),
433 action.to_owned(),
434 status,
435 count,
436 )
437 };
438 assert_eq!(
439 rows,
440 [
441 row("2026-10-23T12:34:00Z", "", "health", 200, 3),
442 row("2026-10-23T12:35:00Z", "", "health", 200, 1),
443 row("2026-10-23T12:34:00Z", "", "health", 503, 1),
444 row("2026-10-23T12:34:00Z", "", "auth_failed", 503, 1),
445 row("2026-10-23T12:34:00Z", "cli", "health", 200, 1),
446 ]
447 );
448 }
449
450 #[test]
451 fn a_record_in_a_transaction_that_does_not_commit_is_not_kept() {
452 let mut log = log();
453 let tx = log.conn.transaction().expect("tx");
454 let key = Key::generate().expect("key").id();
455 record(&tx, &read_by(key, 200), datetime!(2026-10-23 12:00:00 UTC)).expect("record");
456 drop(tx);
457 assert_eq!(exported(&log.conn, None), Vec::<Value>::new());
458 }
459
460 #[test]
461 fn every_action_has_its_name_in_the_log() {
462 let names = [
463 (Action::Create, "create"),
464 (Action::Replace, "replace"),
465 (Action::Read, "read"),
466 (Action::Delete, "delete"),
467 (Action::Purge, "purge"),
468 (Action::LegalRead, "legal_read"),
469 (Action::LegalVersions, "legal_versions"),
470 (Action::Restore, "restore"),
471 (Action::Hold, "hold"),
472 (Action::Release, "release"),
473 (Action::KeyCreate, "key_create"),
474 (Action::KeyRevoke, "key_revoke"),
475 (Action::KeyList, "key_list"),
476 (Action::Retention, "retention"),
477 (Action::Health, "health"),
478 (Action::AuthFailed, "auth_failed"),
479 (Action::TenantCreate, "tenant_create"),
480 (Action::TenantList, "tenant_list"),
481 (Action::TenantRetention, "tenant_retention"),
482 (Action::TenantRemove, "tenant_remove"),
483 (Action::RekeyMaster, "rekey_master"),
484 (Action::RekeyTenant, "rekey_tenant"),
485 (Action::List, "list"),
486 (Action::AuditRead, "audit_read"),
487 (Action::Openapi, "openapi"),
488 ];
489 for (action, name) in names {
490 assert_eq!(action.as_str(), name);
491 }
492 }
493
494 fn five(log: &mut Log) {
496 let key = Key::generate().expect("key").id();
497 let tx = log.conn.transaction().expect("tx");
498 for second in 0..5 {
499 let now = datetime!(2026-10-23 12:00:00 UTC) + time::Duration::seconds(second);
500 record(&tx, &read_by(key, 200), now).expect("record");
501 }
502 tx.commit().expect("commit");
503 }
504
505 fn exported_text(log: &Connection, since: Option<i64>) -> String {
506 let mut out = Vec::new();
507 export(log, since, &mut out).expect("export");
508 String::from_utf8(out).expect("UTF-8")
509 }
510
511 fn paged(log: &Connection, after: i64, limit: u32) -> String {
512 let mut out = Vec::new();
513 page(log, &AuditQuery { after, limit }, &mut out).expect("page");
514 String::from_utf8(out).expect("UTF-8")
515 }
516
517 #[test]
518 fn a_page_is_the_first_lines_export_writes_from_the_same_place() {
519 let mut log = log();
520 five(&mut log);
521 for after in 0..=6 {
522 let lines: Vec<String> = exported_text(&log.conn, Some(after))
523 .lines()
524 .map(|l| format!("{l}\n"))
525 .collect();
526 for limit in 1..=6 {
527 let take = lines.len().min(usize::try_from(limit).expect("small"));
528 assert_eq!(
529 paged(&log.conn, after, limit),
530 lines[..take].concat(),
531 "after {after}, limit {limit}"
532 );
533 }
534 }
535 assert_eq!(paged(&log.conn, 0, 10), exported_text(&log.conn, None));
536 }
537
538 #[test]
539 fn an_audit_query_takes_a_seq_and_a_limit_of_1_to_10000() {
540 let q = |after, limit| AuditQuery { after, limit };
541 assert_eq!(AuditQuery::new(None, None), Ok(q(0, 1000)));
542 assert_eq!(AuditQuery::new(Some(7), Some(1)), Ok(q(7, 1)));
543 assert_eq!(AuditQuery::new(None, Some(10000)), Ok(q(0, 10000)));
544 assert_eq!(
545 AuditQuery::new(Some(i64::MAX as u64), None),
546 Ok(q(i64::MAX, 1000))
547 );
548 for (after, limit) in [
549 (None, Some(0)),
550 (None, Some(10001)),
551 (None, Some(u64::MAX)),
552 (Some(i64::MAX as u64 + 1), None),
553 ] {
554 assert_eq!(
555 AuditQuery::new(after, limit),
556 Err(BadQuery),
557 "{after:?} {limit:?}"
558 );
559 }
560 assert_eq!(AuditQuery::from_query(None), Ok(q(0, 1000)));
561 assert_eq!(
562 AuditQuery::from_query(Some("after=12&limit=3")),
563 Ok(q(12, 3))
564 );
565 for bad in [
566 "after=-1",
567 "after=x",
568 "after=",
569 "limit=0",
570 "limit=1&limit=1",
571 "since=3",
572 "after=1.0",
573 ] {
574 assert_eq!(AuditQuery::from_query(Some(bad)), Err(BadQuery), "{bad}");
575 }
576 }
577}