systemprompt_security/authz/
hook.rs1use std::sync::Arc;
18use std::time::Duration;
19
20use async_trait::async_trait;
21
22use super::audit::{AuthzAuditSink, AuthzSource, NullAuditSink};
23use super::error::AuthzResult;
24use super::types::{AuthzDecision, AuthzRequest};
25
26#[async_trait]
27pub trait AuthzDecisionHook: Send + Sync + std::fmt::Debug {
28 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision;
29}
30
31#[derive(Debug, Clone)]
32pub struct DenyAllHook {
33 sink: Arc<dyn AuthzAuditSink>,
34}
35
36impl DenyAllHook {
37 pub fn new(sink: Arc<dyn AuthzAuditSink>) -> Self {
38 Self { sink }
39 }
40
41 pub fn null() -> Self {
45 Self {
46 sink: Arc::new(NullAuditSink),
47 }
48 }
49}
50
51#[async_trait]
52impl AuthzDecisionHook for DenyAllHook {
53 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
54 let decision = AuthzDecision::Deny {
55 reason: "no authz hook configured".into(),
56 policy: AuthzSource::DenyAllDefault.policy().to_string(),
57 };
58 self.sink
59 .record(&req, &decision, AuthzSource::DenyAllDefault)
60 .await;
61 decision
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct AllowAllHook {
67 sink: Arc<dyn AuthzAuditSink>,
68}
69
70impl AllowAllHook {
71 pub fn new(sink: Arc<dyn AuthzAuditSink>) -> Self {
72 Self { sink }
73 }
74
75 pub fn null() -> Self {
79 Self {
80 sink: Arc::new(NullAuditSink),
81 }
82 }
83}
84
85#[async_trait]
86impl AuthzDecisionHook for AllowAllHook {
87 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
88 let decision = AuthzDecision::Allow;
89 self.sink
90 .record(&req, &decision, AuthzSource::AllowAllUnrestricted)
91 .await;
92 decision
93 }
94}
95
96#[derive(Debug, Clone)]
97pub struct WebhookHook {
98 url: String,
99 timeout: Duration,
100 client: reqwest::Client,
101 sink: Arc<dyn AuthzAuditSink>,
102}
103
104impl WebhookHook {
105 pub fn new(url: String, timeout: Duration, sink: Arc<dyn AuthzAuditSink>) -> AuthzResult<Self> {
106 let client = reqwest::Client::builder().timeout(timeout).build()?;
107 Ok(Self {
108 url,
109 timeout,
110 client,
111 sink,
112 })
113 }
114
115 pub fn url(&self) -> &str {
116 &self.url
117 }
118
119 pub const fn timeout(&self) -> Duration {
120 self.timeout
121 }
122
123 async fn fault(&self, req: &AuthzRequest) -> AuthzDecision {
124 let decision = AuthzDecision::Deny {
125 reason: "authz hook unreachable".into(),
126 policy: AuthzSource::WebhookFault.policy().to_string(),
127 };
128 self.sink
129 .record(req, &decision, AuthzSource::WebhookFault)
130 .await;
131 decision
132 }
133}
134
135#[async_trait]
136impl AuthzDecisionHook for WebhookHook {
137 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
138 let response = self.client.post(&self.url).json(&req).send().await;
139 let response = match response {
140 Ok(r) => r,
141 Err(err) => {
142 tracing::warn!(
143 error = %err,
144 url = %self.url,
145 "authz hook transport failure",
146 );
147 return self.fault(&req).await;
148 },
149 };
150 if !response.status().is_success() {
151 tracing::warn!(
152 status = response.status().as_u16(),
153 url = %self.url,
154 "authz hook returned non-success status",
155 );
156 return self.fault(&req).await;
157 }
158 match response.json::<AuthzDecision>().await {
159 Ok(decision) => decision,
160 Err(err) => {
161 tracing::warn!(
162 error = %err,
163 url = %self.url,
164 "authz hook response decode failure",
165 );
166 self.fault(&req).await
167 },
168 }
169 }
170}