Skip to main content

gatekeep_sqlx/audit/
postgres.rs

1use async_trait::async_trait;
2use gatekeep::{AuditEntry, AuditSink};
3use sqlx::{PgPool, Postgres, Transaction};
4
5use dovecote::EnqueueOutcome;
6use dovecote_sqlx_postgres::PostgresDovecote;
7
8use super::{
9    DecisionAuditConfig, DecisionAuditConfigError, DecisionAuditEventError, event_from_entry,
10};
11
12/// Postgres Dovecote-backed decision audit sink.
13#[derive(Clone)]
14pub struct PgDovecoteAudit {
15    dovecote: PostgresDovecote,
16    config: DecisionAuditConfig,
17}
18
19impl PgDovecoteAudit {
20    /// Creates a sink using an application-owned absolute source URI.
21    ///
22    /// # Errors
23    ///
24    /// Returns an error when `source` is not an absolute URI.
25    pub fn new(pool: PgPool, source: impl Into<String>) -> Result<Self, DecisionAuditConfigError> {
26        let config = DecisionAuditConfig::new(source)?;
27        Ok(Self::from_config(pool, config))
28    }
29
30    /// Creates a sink from validated configuration.
31    #[must_use]
32    pub fn from_config(pool: PgPool, config: DecisionAuditConfig) -> Self {
33        Self {
34            dovecote: PostgresDovecote::new(pool),
35            config,
36        }
37    }
38
39    /// Returns the configuration used for newly constructed events.
40    #[must_use]
41    pub const fn config(&self) -> &DecisionAuditConfig {
42        &self.config
43    }
44
45    /// Verifies that the selected database has the installed Dovecote schema.
46    ///
47    /// # Errors
48    ///
49    /// Returns the typed Dovecote schema error when the schema is absent or
50    /// incompatible.
51    pub async fn check_schema(&self) -> Result<(), dovecote_sqlx_postgres::SchemaError> {
52        self.dovecote.check_schema().await
53    }
54
55    /// Records an audit event in a transaction owned by this sink.
56    ///
57    /// This is atomic between the Dovecote event and its pending delivery. Use
58    /// [`Self::record_decision_audit_in_transaction`] when an application must
59    /// include the event in its own transaction boundary.
60    ///
61    /// # Errors
62    ///
63    /// Returns a typed event, Dovecote, or `SQLx` transaction error.
64    pub async fn record_decision_audit(
65        &self,
66        entry: &AuditEntry,
67    ) -> Result<EnqueueOutcome, PgDovecoteAuditError> {
68        let mut transaction = self.dovecote.pool().begin().await?;
69        let outcome = self
70            .record_decision_audit_in_transaction(&mut transaction, entry)
71            .await?;
72        transaction.commit().await?;
73        Ok(outcome)
74    }
75
76    /// Records an audit event in a caller-owned Postgres transaction.
77    ///
78    /// The caller must commit or roll back the transaction. The operation is
79    /// atomic with other writes in that transaction, but not with arbitrary
80    /// business-state writes performed in another transaction.
81    ///
82    /// # Errors
83    ///
84    /// Returns a typed event or Dovecote error. The caller remains responsible
85    /// for rolling back after an error.
86    pub async fn record_decision_audit_in_transaction(
87        &self,
88        transaction: &mut Transaction<'_, Postgres>,
89        entry: &AuditEntry,
90    ) -> Result<EnqueueOutcome, PgDovecoteAuditError> {
91        let (tenant, event) = event_from_entry(&self.config, entry)?;
92        Ok(self
93            .dovecote
94            .for_tenant(tenant)
95            .enqueue(transaction, event)
96            .await?)
97    }
98}
99
100/// Errors returned by [`PgDovecoteAudit`].
101#[derive(Debug, thiserror::Error)]
102#[non_exhaustive]
103pub enum PgDovecoteAuditError {
104    /// The typed entry could not be converted to a Dovecote event.
105    #[error(transparent)]
106    Event(#[from] DecisionAuditEventError),
107    /// Dovecote rejected the event or database operation.
108    #[error(transparent)]
109    Dovecote(#[from] dovecote_sqlx_postgres::EnqueueError),
110    /// The sink-owned transaction could not begin or commit.
111    #[error(transparent)]
112    Sqlx(#[from] sqlx::Error),
113}
114
115#[async_trait]
116impl AuditSink for PgDovecoteAudit {
117    type Error = PgDovecoteAuditError;
118
119    async fn record(&self, entry: &AuditEntry) -> Result<(), Self::Error> {
120        self.record_decision_audit(entry).await.map(|_| ())
121    }
122}