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