Skip to main content

systemprompt_security/authz/audit/
mod.rs

1//! Audit sink for authorization decisions.
2//!
3//! Every decision made *inside core* (webhook fault, default deny,
4//! unrestricted allow) flows through an [`AuthzAuditSink`] so it lands in the
5//! same `governance_decisions` table the extension's `POST /govern/authz`
6//! handler writes to. Successful webhook round-trips are audited by the
7//! extension itself (single writer per code path); core's sink only records
8//! decisions the extension never sees.
9//!
10//! [`NullAuditSink`] is the bootstrap default — it exists so unit tests and
11//! pre-database bootstrap stages can install hooks without a `DbPool`.
12//! Production replaces it with [`DbAuditSink`] once the database is available.
13
14mod db_sink;
15mod repository;
16
17use async_trait::async_trait;
18
19use super::types::{AuthzDecision, AuthzRequest};
20
21pub use db_sink::DbAuditSink;
22pub use repository::{
23    GovernanceDecisionRecord, GovernanceDecisionRepository, insert_governance_decision,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum AuthzSource {
28    WebhookFault,
29    DenyAllDefault,
30    AllowAllUnrestricted,
31}
32
33impl AuthzSource {
34    pub const fn policy(self) -> &'static str {
35        match self {
36            Self::WebhookFault => "authz_hook_fault",
37            Self::DenyAllDefault => "authz_default_deny",
38            Self::AllowAllUnrestricted => "authz_unrestricted",
39        }
40    }
41}
42
43#[async_trait]
44pub trait AuthzAuditSink: Send + Sync + std::fmt::Debug {
45    async fn record(&self, req: &AuthzRequest, decision: &AuthzDecision, source: AuthzSource);
46}
47
48#[derive(Debug, Default, Clone, Copy)]
49pub struct NullAuditSink;
50
51#[async_trait]
52impl AuthzAuditSink for NullAuditSink {
53    async fn record(&self, _req: &AuthzRequest, _decision: &AuthzDecision, _source: AuthzSource) {}
54}