Skip to main content

yeti_types/schema/
audit.rs

1//! `@audit` directive types and the global audit sender.
2
3use std::collections::HashSet;
4
5// ============================================================================
6// @audit directive
7// ============================================================================
8
9/// Operations that can be audited.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
11pub enum AuditOp {
12    /// A read operation was performed.
13    Read,
14    /// A create operation was performed.
15    Create,
16    /// An update operation was performed.
17    Update,
18    /// A delete operation was performed.
19    Delete,
20}
21
22impl AuditOp {
23    /// Parse an audit operation from a string (case-insensitive).
24    #[must_use]
25    pub fn parse(s: &str) -> Option<Self> {
26        match s.to_lowercase().as_str() {
27            "read" => Some(Self::Read),
28            "create" => Some(Self::Create),
29            "update" => Some(Self::Update),
30            "delete" => Some(Self::Delete),
31            _ => None,
32        }
33    }
34}
35
36/// Audit configuration from `@audit` directive.
37///
38/// Bare `@audit` enables auditing with defaults: all mutations, 90-day retention,
39/// no state capture. Read auditing excluded from default (opt in with `read: true`).
40#[derive(Debug, Clone)]
41pub struct AuditConfig {
42    /// Which operations to audit (default: create, update, delete).
43    pub operations: HashSet<AuditOp>,
44    /// Retention period in days (default: 90, 0 = forever).
45    pub retention_days: u64,
46    /// Capture before/after record state on mutations (default: false).
47    pub capture_state: bool,
48    /// Log read operations to the `TransactionLog` (default: false).
49    ///
50    /// `@audit(read: true)` enables read auditing for this table.
51    /// Reads that scope `WriteContext` are gated by this flag so the
52    /// `TransactionLog` stays clean by default.
53    pub log_reads: bool,
54    /// Log write operations to the `TransactionLog` (default: true).
55    ///
56    /// `@audit(write: false)` disables write auditing for this table.
57    pub log_writes: bool,
58}
59
60impl Default for AuditConfig {
61    fn default() -> Self {
62        let mut operations = HashSet::new();
63        operations.insert(AuditOp::Create);
64        operations.insert(AuditOp::Update);
65        operations.insert(AuditOp::Delete);
66        Self {
67            operations,
68            retention_days: 90,
69            capture_state: false,
70            log_reads: false,
71            log_writes: true,
72        }
73    }
74}
75
76/// A table-level audit entry emitted after successful CRUD operations.
77///
78/// Sent via `try_send` (fire-and-forget) on the audit channel — never blocks
79/// the write path. Dropped silently if the channel is full.
80#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
81pub struct AuditEntry {
82    /// Epoch milliseconds
83    pub timestamp: f64,
84    /// Application that owns the table
85    pub app_id: String,
86    /// Table name
87    pub table: String,
88    /// Record ID
89    pub record_id: String,
90    /// What happened
91    pub operation: AuditOp,
92    /// Who did it (username or "anonymous")
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub user: Option<String>,
95    /// Record state after the operation (for create/update)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub after: Option<serde_json::Value>,
98    /// Record state before the operation (only when `capture_state`: true)
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub before: Option<serde_json::Value>,
101    /// Interface that triggered the operation (e.g., "rest", "graphql", "mcp", "mqtt")
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub interface: Option<String>,
104}
105
106// ============================================================================
107// `AuditEntry`, `AuditOp`, and `AuditConfig` above are the HTTP response
108// shape for /{app}/audit — yeti-audit::AuditResource materializes them
109// from LogEntry on read. Audit durability is handled by the
110// LoggingBackend wrapper which appends a LogEntry per WriteOp to the
111// unified TransactionLog (yeti_types::backend::TransactionLog).
112// ============================================================================