gatekeep_sqlx/audit/
postgres.rs1use async_trait::async_trait;
2use gatekeep::AuditEntry;
3use sqlx::Transaction;
4
5use super::support::{
6 deny_shape_label, effect_label, position_i32, presence_label, records_from_json_rows,
7};
8use super::{DecisionAuditRecord, SqlxAuditError, SqlxAuditStore, SqlxDecisionAuditRepository};
9
10pub type PgDecisionAuditRepository = SqlxDecisionAuditRepository<crate::PostgresBackend>;
12
13impl PgDecisionAuditRepository {
14 #[must_use]
16 pub const fn new(pool: sqlx::PgPool) -> Self {
17 Self::from_pool(pool)
18 }
19}
20
21#[async_trait]
22impl SqlxAuditStore<crate::PostgresBackend> for PgDecisionAuditRepository {
23 async fn record_decision_audit(&self, entry: &AuditEntry) -> Result<i64, SqlxAuditError> {
24 let mut tx = self.pool.begin().await?;
25 let denial_reason_json = entry
26 .denial_reason
27 .as_ref()
28 .map(serde_json::to_value)
29 .transpose()?;
30 let id = sqlx::query_scalar::<_, i64>(
31 r"
32 insert into gatekeep_audit_decisions
33 (request_id, policy_id, policy_hash, effect, trace, decisive_clause,
34 denial_reason_code, denial_reason_shape, denial_reason, entry)
35 values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
36 returning id
37 ",
38 )
39 .bind(entry.request_id.as_ref().map(gatekeep::RequestId::as_str))
40 .bind(entry.anchor.policy_id.as_str())
41 .bind(entry.anchor.policy_hash.as_str())
42 .bind(effect_label(entry))
43 .bind(serde_json::to_value(&entry.trace)?)
44 .bind(serde_json::to_value(&entry.decisive)?)
45 .bind(
46 entry
47 .denial_reason
48 .as_ref()
49 .map(|reason| reason.code.as_str()),
50 )
51 .bind(
52 entry
53 .denial_reason
54 .as_ref()
55 .map(|reason| deny_shape_label(reason.shape)),
56 )
57 .bind(denial_reason_json)
58 .bind(serde_json::to_value(entry)?)
59 .fetch_one(&mut *tx)
60 .await?;
61 insert_children(&mut tx, id, entry).await?;
62 insert_outbox(&mut tx, id, entry).await?;
63 tx.commit().await?;
64 Ok(id)
65 }
66
67 async fn decision_audit_records(
68 &self,
69 after_id: Option<i64>,
70 limit: i64,
71 ) -> Result<Vec<DecisionAuditRecord>, SqlxAuditError> {
72 let rows = sqlx::query(
73 "select id, entry from gatekeep_audit_decisions where ($1::bigint is null or id > $1) order by id limit $2",
74 )
75 .bind(after_id)
76 .bind(limit)
77 .fetch_all(&self.pool)
78 .await?;
79 records_from_json_rows(rows)
80 }
81}
82
83async fn insert_children(
84 tx: &mut Transaction<'_, sqlx::Postgres>,
85 decision_id: i64,
86 entry: &AuditEntry,
87) -> Result<(), SqlxAuditError> {
88 for (position, obligation) in entry.obligations.iter().enumerate() {
89 sqlx::query(
90 "insert into gatekeep_audit_obligations (decision_id, position, obligation_id) values ($1, $2, $3)",
91 )
92 .bind(decision_id)
93 .bind(position_i32(position))
94 .bind(obligation.as_str())
95 .execute(&mut **tx)
96 .await?;
97 }
98 for (position, (fact, presence)) in entry.consulted.iter().enumerate() {
99 sqlx::query(
100 "insert into gatekeep_audit_consulted_facts (decision_id, position, fact_id, presence) values ($1, $2, $3, $4)",
101 )
102 .bind(decision_id)
103 .bind(position_i32(position))
104 .bind(fact.as_str())
105 .bind(presence_label(*presence))
106 .execute(&mut **tx)
107 .await?;
108 }
109 for (slot, subject) in &entry.subjects {
110 sqlx::query(
111 "insert into gatekeep_audit_request_subjects (decision_id, slot, subject_kind, subject_id) values ($1, $2, $3, $4)",
112 )
113 .bind(decision_id)
114 .bind(slot.as_str())
115 .bind(subject.kind())
116 .bind(subject.id())
117 .execute(&mut **tx)
118 .await?;
119 }
120 if let Some(reason) = &entry.denial_reason {
121 for (key, value) in &reason.params {
122 sqlx::query(
123 "insert into gatekeep_audit_reason_params (decision_id, key, value) values ($1, $2, $3)",
124 )
125 .bind(decision_id)
126 .bind(key.as_str())
127 .bind(serde_json::to_value(value)?)
128 .execute(&mut **tx)
129 .await?;
130 }
131 }
132 Ok(())
133}
134
135async fn insert_outbox(
136 tx: &mut Transaction<'_, sqlx::Postgres>,
137 decision_id: i64,
138 entry: &AuditEntry,
139) -> Result<(), SqlxAuditError> {
140 sqlx::query("insert into gatekeep_audit_outbox (decision_id, payload) values ($1, $2)")
141 .bind(decision_id)
142 .bind(serde_json::to_value(entry)?)
143 .execute(&mut **tx)
144 .await?;
145 Ok(())
146}