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