gatekeep-sqlx 5.0.0

SQLx query lowering adapter for gatekeep
Documentation
use super::AttemptAuditEventError;
use super::attempt::event_from_attempt;
use async_trait::async_trait;
use gatekeep::{AttemptAuditSink, AuditEntry, AuditSink, AuthorizationAttempt};
use sqlx::{MySql, MySqlPool, Transaction};

use dovecote::EnqueueOutcome;
use dovecote_sqlx_mysql::MySqlDovecote;

use super::{
    DecisionAuditConfig, DecisionAuditConfigError, DecisionAuditEventError, event_from_entry,
};

/// MySQL/MariaDB Dovecote-backed decision audit sink.
#[derive(Clone)]
pub struct MySqlDovecoteAudit {
    dovecote: MySqlDovecote,
    config: DecisionAuditConfig,
}

impl MySqlDovecoteAudit {
    /// Creates a sink using an application-owned absolute source URI.
    ///
    /// # Errors
    ///
    /// Returns an error when `source` is not an absolute URI.
    pub fn new(
        pool: MySqlPool,
        source: impl Into<String>,
    ) -> Result<Self, DecisionAuditConfigError> {
        let config = DecisionAuditConfig::new(source)?;
        Ok(Self::from_config(pool, config))
    }

    /// Creates a sink from validated configuration.
    #[must_use]
    pub fn from_config(pool: MySqlPool, config: DecisionAuditConfig) -> Self {
        Self {
            dovecote: MySqlDovecote::new(pool),
            config,
        }
    }

    /// Returns the configuration used for newly constructed events.
    #[must_use]
    pub const fn config(&self) -> &DecisionAuditConfig {
        &self.config
    }

    /// Verifies that the selected database has the installed Dovecote schema.
    ///
    /// # Errors
    ///
    /// Returns the typed Dovecote schema error when the schema is absent or
    /// incompatible.
    pub async fn check_schema(&self) -> Result<(), dovecote_sqlx_mysql::SchemaError> {
        self.dovecote.check_schema().await
    }

    /// Stores a failed attempt in its own transaction.
    ///
    /// # Errors
    /// Returns event validation, enqueue or commit errors. Retain the same
    /// immutable entry when reconciling an uncertain commit.
    pub async fn record_authorization_attempt(
        &self,
        entry: &AuthorizationAttempt,
    ) -> Result<EnqueueOutcome, MySqlDovecoteAuditError> {
        let mut transaction = self.dovecote.pool().begin().await?;
        let outcome = self
            .record_attempt_in_transaction(&mut transaction, entry)
            .await?;
        transaction.commit().await?;
        Ok(outcome)
    }

    /// Stores a failed attempt in a caller-owned transaction.
    ///
    /// # Errors
    /// Returns event or enqueue failures. The caller must commit for durability;
    /// rolling back the protected operation also rolls back this audit event.
    pub async fn record_attempt_in_transaction(
        &self,
        transaction: &mut Transaction<'_, MySql>,
        entry: &AuthorizationAttempt,
    ) -> Result<EnqueueOutcome, MySqlDovecoteAuditError> {
        let (tenant, event) = event_from_attempt(&self.config, entry)?;
        Ok(self
            .dovecote
            .for_tenant(tenant)
            .enqueue(transaction, event)
            .await?)
    }

    /// Records an audit event in a transaction owned by this sink.
    ///
    /// This is atomic between the Dovecote event and its pending delivery. Use
    /// [`Self::record_decision_audit_in_transaction`] when an application must
    /// include the event in its own transaction boundary.
    ///
    /// # Errors
    ///
    /// Returns a typed event, Dovecote, or `SQLx` transaction error.
    pub async fn record_decision_audit(
        &self,
        entry: &AuditEntry,
    ) -> Result<EnqueueOutcome, MySqlDovecoteAuditError> {
        let mut transaction = self.dovecote.pool().begin().await?;
        let outcome = self
            .record_decision_audit_in_transaction(&mut transaction, entry)
            .await?;
        transaction.commit().await?;
        Ok(outcome)
    }

    /// Records an audit event in a caller-owned MySQL/MariaDB transaction.
    ///
    /// The caller must commit or roll back the transaction. The operation is
    /// atomic with other writes in that transaction, but not with arbitrary
    /// business-state writes performed in another transaction.
    ///
    /// # Errors
    ///
    /// Returns a typed event or Dovecote error. The caller remains responsible
    /// for rolling back after an error.
    pub async fn record_decision_audit_in_transaction(
        &self,
        transaction: &mut Transaction<'_, MySql>,
        entry: &AuditEntry,
    ) -> Result<EnqueueOutcome, MySqlDovecoteAuditError> {
        let (tenant, event) = event_from_entry(&self.config, entry)?;
        Ok(self
            .dovecote
            .for_tenant(tenant)
            .enqueue(transaction, event)
            .await?)
    }
}

/// Errors returned by [`MySqlDovecoteAudit`].
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MySqlDovecoteAuditError {
    /// Failed-attempt payload or event validation error.
    #[error(transparent)]
    Attempt(#[from] AttemptAuditEventError),
    /// The typed entry could not be converted to a Dovecote event.
    #[error(transparent)]
    Event(#[from] DecisionAuditEventError),
    /// Dovecote rejected the event or database operation.
    #[error(transparent)]
    Dovecote(#[from] dovecote_sqlx_mysql::EnqueueError),
    /// The sink-owned transaction could not begin or commit.
    #[error(transparent)]
    Sqlx(#[from] sqlx::Error),
}

#[async_trait]
impl AuditSink for MySqlDovecoteAudit {
    type Error = MySqlDovecoteAuditError;

    async fn record(&self, entry: &AuditEntry) -> Result<(), Self::Error> {
        self.record_decision_audit(entry).await.map(|_| ())
    }
}

#[async_trait]
impl AttemptAuditSink for MySqlDovecoteAudit {
    type Error = MySqlDovecoteAuditError;
    async fn record_attempt(&self, entry: &AuthorizationAttempt) -> Result<(), Self::Error> {
        self.record_authorization_attempt(entry).await.map(|_| ())
    }
}