Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/admin/audit.rs

4.8 KiB, 44 runs

created by r1870400018:10430, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Admin audit log.
2//!
3//! Append-only line-delimited file recording every privileged
4//! action taken against the wallet, the dashboard, or anything
5//! else the operator needs an after-the-fact paper trail for.
6//!
7//! Originally introduced for the CLI `admin --add` / `admin --remove`
8//! verbs and the wallet-v2 migration; lifted out of `app/repl.rs`
9//! into the admin module so the dashboard handler in
10//! `srv/admin/handler.rs` can write to the same file using the same
11//! line format.
12//!
13//! # Format
14//!
15//! One entry per line:
16//!
17//! ```text
18//! <unix_seconds> <admin> <verb> <result> <detail>
19//! ```
20//!
21//! - `unix_seconds` -- seconds since epoch when the event was recorded.
22//! - `admin` -- name of the admin who triggered the action, or one of
23//! the sentinels `(unknown)` / `(anon)` when no identity was
24//! captured.
25//! - `verb` -- dotted action name such as `admin.add`, `dashboard.login`.
26//! - `result` -- `ok` or `err`.
27//! - `detail` -- free-form key=value pairs, space-separated, no
28//! newlines. Quote values that contain spaces.
29//!
30//! The format is deliberately greppable rather than structured;
31//! the audit log is read by humans in incident response, not by
32//! parsers.
33//!
34//! # Failure handling
35//!
36//! Failures to open or write the log file are logged at `warn!` and
37//! never propagated. The principle: the action being audited must
38//! not fail because the audit log is unavailable. A dashboard login
39//! that succeeds against a healthy wallet should still let the
40//! operator in even if the disk holding the audit log is full.
41//!
42//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
43//! Anthropic Claude
44
45use oxedyne_fe2o3_core::prelude::*;
46
47use std::{
48 io::Write,
49 path::{
50 Path,
51 PathBuf,
52 },
53 time::{
54 SystemTime,
55 UNIX_EPOCH,
56 },
57};
58
59// Hosted here rather than in `app::constant` so the dashboard handler in the
60// `srv` layer does not have to import from `app`.
61pub const ADMIN_AUDIT_LOG_NAME: &str = "admin-audit.log";
62
63// ┌───────────────────────────────────────────────────────────────────────────┐
64// │ VERBS │
65// └───────────────────────────────────────────────────────────────────────────┘
66
67// CLI-side verbs (`admin.add`, `admin.remove`, `admin.passwd`, `admin.list`,
68// `wallet.migrate`) stay as inline strings in `app/repl.rs` so the original call
69// sites remain stable; dashboard verbs are declared here so handler call sites
70// cannot drift on spelling.
71pub const VERB_DASHBOARD_LOGIN: &str = "dashboard.login";
72pub const VERB_DASHBOARD_LOGOUT: &str = "dashboard.logout";
73pub const VERB_DASHBOARD_ADMIN_ADD: &str = "dashboard.admin.add";
74pub const VERB_DASHBOARD_ADMIN_REMOVE: &str = "dashboard.admin.remove";
75pub const VERB_DASHBOARD_GUARD_WHITELIST: &str = "dashboard.guard.whitelist";
76pub const VERB_DASHBOARD_GUARD_BLACKLIST: &str = "dashboard.guard.blacklist";
77pub const VERB_DASHBOARD_GUARD_UNBLOCK: &str = "dashboard.guard.unblock";
78
79pub const ADMIN_ANON: &str = "(anon)"; // unauthenticated visitor, e.g. a failed login
80
81// ┌───────────────────────────────────────────────────────────────────────────┐
82// │ APPEND │
83// └───────────────────────────────────────────────────────────────────────────┘
84
85/// Failures are logged at `warn!` and swallowed. The audit log is never allowed
86/// to break the action it is recording.
87pub fn append(admin: &str, verb: &str, result: &str, detail: &str) {
88 let secs = SystemTime::now()
89 .duration_since(UNIX_EPOCH)
90 .map(|d| d.as_secs())
91 .unwrap_or(0);
92 let line = fmt!(
93 "{} {} {} {} {}\n",
94 secs, admin, verb, result, detail,
95 );
96 let path: PathBuf = Path::new("./").join(ADMIN_AUDIT_LOG_NAME);
97 match std::fs::OpenOptions::new()
98 .create(true)
99 .append(true)
100 .open(&path)
101 {
102 Ok(mut f) => {
103 if let Err(e) = f.write_all(line.as_bytes()) {
104 warn!("Failed to write audit log line: {}", e);
105 }
106 },
107 Err(e) => warn!("Failed to open audit log {:?}: {}", path, e),
108 }
109}