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