adminx_audit/lib.rs
1// adminx-audit/src/lib.rs
2//
3// Audit logging for adminx. Register it and every create / update / delete that
4// goes through the default `Resource` CRUD is recorded with who did it and a
5// per-column before/after diff; leave it out and adminx behaves exactly as it
6// did, issuing not one extra query.
7//
8// Storage-agnostic: rows are written through adminx-core's `Storage` trait, so
9// the same crate works over SeaORM (SQL) or MongoDB. The one asymmetry is table
10// creation — see `migrate_sql`.
11//
12// ## Startup order
13//
14// ```ignore
15// adminx_seaorm::init(&db_url).await?; // 1. storage
16// adminx_core::seed(adminx_audit::migrate_sql()).await?; // 2. SQL table (SQL backends only)
17// adminx_audit::init(AuditConfig::default()); // 3. register the auditor
18// configure_auth(AuthConfig { /* ... */ }); // 4. turn auth on
19// register_resource(Box::new(MyResource));
20// adminx_audit::register_resources(); // 5. the in-panel viewer (optional)
21// ```
22//
23// ## What is and isn't recorded
24//
25// The hook lives on the `Resource` trait's default `create` / `update` /
26// `delete`, because only that layer holds the `ReqCtx` that identifies the
27// actor. A resource that *extends* the defaults keeps its recording as long as
28// it delegates to `adminx_core::crud::{create, update, delete}` rather than
29// copying the body — `adminx-rbac`'s `PermissionResource` does exactly that. A
30// resource that replaces them outright records nothing unless it emits its own
31// entry; see [`adminx_core::audit::emit`].
32//
33// Writes that bypass adminx entirely (a migration, psql, another service) are
34// invisible to this crate by construction. If you need those too, the answer is
35// database triggers, not an application-level log.
36
37mod resource;
38mod schema;
39mod store;
40
41pub use resource::AuditVersionResource;
42pub use store::{StorageAuditor, TABLE};
43
44/// How the auditor behaves when it cannot write.
45#[derive(Debug, Clone, Copy, Default)]
46pub struct AuditConfig {
47 /// When `true`, a failed audit write turns the request into a 500 instead of
48 /// only logging the error.
49 ///
50 /// Note what this does *not* buy you: the mutation has already committed by
51 /// then, and `Storage` exposes no transaction spanning both writes, so a
52 /// strict failure reports an error for a change that did land. It makes a
53 /// hole in the log loud rather than silent — it is not atomicity. Leave it
54 /// `false` (the default) unless a compliance regime prefers a visible
55 /// failure to a missing entry.
56 pub strict: bool,
57}
58
59impl AuditConfig {
60 /// Fail the request when an entry cannot be recorded. See the caveat on
61 /// [`AuditConfig::strict`].
62 pub fn strict() -> Self {
63 Self { strict: true }
64 }
65}
66
67/// Register the auditor with adminx-core. Call after storage is set (and, on a
68/// SQL backend, after running [`migrate_sql`]).
69///
70/// Set-once, matching `set_storage` / `set_authorizer`: a second call is ignored
71/// with a warning.
72pub fn init(config: AuditConfig) {
73 adminx_core::set_auditor(Box::new(StorageAuditor::new(config.strict)));
74 tracing::info!(
75 "adminx-audit: recording to `{TABLE}` (strict={})",
76 config.strict
77 );
78}
79
80/// SQL `CREATE TABLE IF NOT EXISTS` + index statements for the audit table. Run
81/// once on a SQL backend via `adminx_core::seed(adminx_audit::migrate_sql())`.
82/// Mongo needs nothing (collections auto-create).
83pub fn migrate_sql() -> &'static [&'static str] {
84 schema::SQL
85}
86
87/// Register the read-only in-panel log viewer at `/adminx/adminx-audit-versions/list`.
88/// Optional — omit it to keep the log out of the UI and query it directly.
89pub fn register_resources() {
90 adminx_core::register_resource(Box::new(AuditVersionResource));
91}