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