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