Skip to main content

khive_gate/
audit.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::{ActorRef, GateDecision, Obligation};
5
6/// Structured audit record emitted once per gate consultation.
7///
8/// JSON field names are stable; events reach tracing and the configured event store. See
9/// `crates/khive-gate/docs/api/audit-events.md`.
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct AuditEvent {
12    /// Wall-clock timestamp of the gate check (UTC, RFC3339 in JSON).
13    pub timestamp: DateTime<Utc>,
14    /// Caller identity as given to the gate.
15    pub actor: ActorRef,
16    /// Namespace in which the verb was invoked.
17    pub namespace: String,
18    /// Verb being dispatched.
19    pub verb: String,
20    /// Gate outcome — `"allow"`, `"deny"`, or `"gate_unavailable"`.
21    pub decision: AuditDecision,
22    /// Deny reason, present only when `decision == "deny"`.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub deny_reason: Option<String>,
25    /// Obligations on allow; always serialized and empty on deny or outage.
26    #[serde(default)]
27    pub obligations: Vec<Obligation>,
28    /// Name of the gate implementation that produced this decision.
29    pub gate_impl: String,
30    /// Correlation token — `GateContext::session_id` when present, else `None`.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub session_id: Option<String>,
33}
34
35/// The outcome field of an [`AuditEvent`].
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum AuditDecision {
39    Allow,
40    Deny,
41    GateUnavailable,
42}
43
44impl AuditEvent {
45    /// Project one request/decision pair into a timestamped stable audit envelope.
46    ///
47    /// See `crates/khive-gate/docs/api/audit-events.md`.
48    pub fn from_check(req: &crate::GateRequest, decision: &GateDecision, gate_impl: &str) -> Self {
49        let (audit_decision, deny_reason, obligations) = match decision {
50            GateDecision::Allow { obligations } => {
51                (AuditDecision::Allow, None, obligations.clone())
52            }
53            GateDecision::Deny { reason } => {
54                (AuditDecision::Deny, Some(reason.clone()), Vec::new())
55            }
56        };
57        Self {
58            timestamp: req.context.timestamp.unwrap_or_else(chrono::Utc::now),
59            actor: req.actor.clone(),
60            namespace: req.namespace.as_str().to_string(),
61            verb: req.verb.clone(),
62            decision: audit_decision,
63            deny_reason,
64            obligations,
65            gate_impl: gate_impl.to_string(),
66            session_id: req.context.session_id.clone(),
67        }
68    }
69
70    /// Project a gate infrastructure failure into the stable audit envelope.
71    pub fn gate_unavailable(req: &crate::GateRequest, gate_impl: &str) -> Self {
72        Self {
73            timestamp: req.context.timestamp.unwrap_or_else(chrono::Utc::now),
74            actor: req.actor.clone(),
75            namespace: req.namespace.as_str().to_string(),
76            verb: req.verb.clone(),
77            decision: AuditDecision::GateUnavailable,
78            deny_reason: None,
79            obligations: Vec::new(),
80            gate_impl: gate_impl.to_string(),
81            session_id: req.context.session_id.clone(),
82        }
83    }
84}