Skip to main content

gatekeep_sqlx/audit/
postgres.rs

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