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