Skip to main content

corium_authz/
audit.rs

1//! Audit events for authorization decisions.
2//!
3//! Every decision is a natural audit record, and the pieces a reviewer needs
4//! are exactly what the check already has: who asked, what they asked for,
5//! what was decided, the authz basis `t` the decision was made against, the
6//! relationship path that granted it, and the view filter it was narrowed
7//! through. Recording `authz_t` is what makes a decision reproducible — the
8//! authz database read `as-of` that `t` yields the policy that produced it.
9
10use corium_protocol::authz::{Access, Principal};
11
12use crate::model::action_name;
13
14/// One authorization decision, as recorded.
15#[derive(Clone, Debug)]
16pub struct AuditEvent {
17    /// Subject of the principal that asked.
18    pub subject: String,
19    /// Provider that vouched for it.
20    pub provider: String,
21    /// Action name.
22    pub action: &'static str,
23    /// Target database, when the action names one.
24    pub database: Option<String>,
25    /// Target object the check ran against.
26    pub object: String,
27    /// Whether the access was permitted.
28    pub allowed: bool,
29    /// Reason, when denied.
30    pub reason: Option<String>,
31    /// Relationship path that granted the access.
32    pub path: Option<String>,
33    /// Views the decision was narrowed through.
34    pub views: Vec<String>,
35    /// Authz database basis the decision was made against.
36    pub authz_t: u64,
37    /// Name of the authz database.
38    pub source: String,
39}
40
41impl AuditEvent {
42    /// Builds an event from a decision.
43    #[must_use]
44    pub fn new(
45        principal: &Principal,
46        access: &Access,
47        decision: &crate::AuthzDecision,
48        source: &str,
49    ) -> Self {
50        Self {
51            subject: principal.subject.clone(),
52            provider: principal.provider.clone(),
53            action: action_name(access.action),
54            database: access.database.clone(),
55            object: decision.object.clone(),
56            allowed: decision.is_allowed(),
57            reason: decision.reason.clone(),
58            path: decision.path.clone(),
59            views: decision.views.clone(),
60            authz_t: decision.authz_t,
61            source: source.to_owned(),
62        }
63    }
64}
65
66/// Receives audit events.
67pub trait AuditSink: Send + Sync + 'static {
68    /// Records one decision.
69    fn record(&self, event: &AuditEvent);
70}
71
72/// The default sink: `tracing`, denials at `info` and grants at `debug`, both
73/// under the `corium_authz::audit` target so a deployment can route them.
74#[derive(Clone, Copy, Debug, Default)]
75pub struct TracingAudit;
76
77impl AuditSink for TracingAudit {
78    fn record(&self, event: &AuditEvent) {
79        if event.allowed {
80            tracing::debug!(
81                target: "corium_authz::audit",
82                subject = %event.subject,
83                provider = %event.provider,
84                action = event.action,
85                object = %event.object,
86                path = event.path.as_deref().unwrap_or(""),
87                views = ?event.views,
88                authz_t = event.authz_t,
89                source = %event.source,
90                "authorization allowed"
91            );
92        } else {
93            tracing::info!(
94                target: "corium_authz::audit",
95                subject = %event.subject,
96                provider = %event.provider,
97                action = event.action,
98                object = %event.object,
99                reason = event.reason.as_deref().unwrap_or(""),
100                authz_t = event.authz_t,
101                source = %event.source,
102                "authorization denied"
103            );
104        }
105    }
106}
107
108/// Discards every event.
109#[derive(Clone, Copy, Debug, Default)]
110pub struct NullAudit;
111
112impl AuditSink for NullAudit {
113    fn record(&self, _event: &AuditEvent) {}
114}