Skip to main content

gatekeep_sqlx/audit/
mod.rs

1use std::marker::PhantomData;
2
3use async_trait::async_trait;
4use gatekeep::{AuditEntry, AuditSink};
5use serde::{Deserialize, Serialize};
6use sqlx::Pool;
7
8use crate::GatekeepSqlxBackend;
9
10#[cfg(feature = "mysql")]
11mod mysql;
12#[cfg(feature = "postgres")]
13mod postgres;
14#[cfg(feature = "sqlite")]
15mod sqlite;
16mod support;
17
18#[cfg(feature = "mysql")]
19pub use self::mysql::MySqlDecisionAuditRepository;
20#[cfg(feature = "postgres")]
21pub use self::postgres::PgDecisionAuditRepository;
22#[cfg(feature = "sqlite")]
23pub use self::sqlite::SqliteDecisionAuditRepository;
24
25/// Persisted decision audit entry.
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub struct DecisionAuditRecord {
28    /// Monotonic row id for cursor export.
29    pub id: i64,
30    /// Reconstructed typed audit entry.
31    pub entry: AuditEntry,
32}
33
34/// `SQLx` audit repository errors.
35#[derive(Debug, thiserror::Error)]
36#[non_exhaustive]
37pub enum SqlxAuditError {
38    /// `SQLx` returned an error.
39    #[error(transparent)]
40    Sqlx(#[from] sqlx::Error),
41    /// Audit entry JSON could not be encoded or decoded.
42    #[error(transparent)]
43    Json(#[from] serde_json::Error),
44    /// Inserted row id did not fit in the portable audit id type.
45    #[error("audit row id {id} does not fit in i64")]
46    IdOverflow {
47        /// Backend row id.
48        id: u64,
49    },
50}
51
52/// SQLx-side decision audit contract.
53#[async_trait]
54pub trait SqlxAuditStore<B>: Send + Sync
55where
56    B: GatekeepSqlxBackend,
57{
58    /// Records one authorization decision.
59    async fn record_decision_audit(&self, entry: &AuditEntry) -> Result<i64, SqlxAuditError>;
60
61    /// Reads decision audit records in stable id order.
62    async fn decision_audit_records(
63        &self,
64        after_id: Option<i64>,
65        limit: i64,
66    ) -> Result<Vec<DecisionAuditRecord>, SqlxAuditError>;
67}
68
69/// SQLx-backed decision audit repository.
70#[derive(Debug)]
71pub struct SqlxDecisionAuditRepository<B>
72where
73    B: GatekeepSqlxBackend,
74{
75    pub(crate) pool: Pool<B::Database>,
76    backend: PhantomData<fn() -> B>,
77}
78
79impl<B> Clone for SqlxDecisionAuditRepository<B>
80where
81    B: GatekeepSqlxBackend,
82{
83    fn clone(&self) -> Self {
84        Self {
85            pool: self.pool.clone(),
86            backend: PhantomData,
87        }
88    }
89}
90
91impl<B> SqlxDecisionAuditRepository<B>
92where
93    B: GatekeepSqlxBackend,
94{
95    pub(crate) const fn from_pool(pool: Pool<B::Database>) -> Self {
96        Self {
97            pool,
98            backend: PhantomData,
99        }
100    }
101}
102
103#[async_trait]
104impl<B> AuditSink for SqlxDecisionAuditRepository<B>
105where
106    B: GatekeepSqlxBackend,
107    Self: SqlxAuditStore<B>,
108{
109    type Error = SqlxAuditError;
110
111    async fn record(&self, entry: &AuditEntry) -> Result<(), Self::Error> {
112        self.record_decision_audit(entry).await?;
113        Ok(())
114    }
115}