systemprompt_security/authz/
runtime.rs1use std::sync::Arc;
25use std::time::Duration;
26
27use systemprompt_models::net::validate_outbound_url;
28use systemprompt_models::profile::{AuthzMode, GovernanceConfig, UNRESTRICTED_ACKNOWLEDGEMENT};
29
30use super::audit::{AuthzAuditSink, DbAuditSink, GovernanceDecisionRepository, NullAuditSink};
31use super::error::{AuthzBootstrapError, AuthzResult};
32use super::hook::{AllowAllHook, AuthzDecisionHook, DenyAllHook, WebhookHook};
33
34pub type SharedAuthzHook = Arc<dyn AuthzDecisionHook>;
35
36pub fn build_authz_hook(
37 governance: Option<&GovernanceConfig>,
38 pool: Option<Arc<sqlx::PgPool>>,
39) -> AuthzResult<SharedAuthzHook> {
40 let sink = build_sink(pool);
41
42 let Some(authz) = governance.and_then(|g| g.authz.as_ref()) else {
43 tracing::error!(
44 "governance.authz block missing — using DenyAllHook (all requests will be denied)"
45 );
46 return Ok(Arc::new(DenyAllHook::new(sink)));
47 };
48
49 match authz.hook.mode {
50 AuthzMode::Disabled => {
51 tracing::warn!("governance.authz.hook.mode = disabled — all requests will be denied");
52 Ok(Arc::new(DenyAllHook::new(sink)))
53 },
54 AuthzMode::Unrestricted => {
55 let ack = authz.hook.acknowledgement.as_deref().map_or("", str::trim);
56 if ack != UNRESTRICTED_ACKNOWLEDGEMENT {
57 return Err(AuthzBootstrapError::MissingUnrestrictedAcknowledgement {
58 expected: UNRESTRICTED_ACKNOWLEDGEMENT,
59 }
60 .into());
61 }
62 tracing::error!(
63 "governance.authz.hook.mode = unrestricted — ALL REQUESTS WILL BE ALLOWED. This \
64 MUST NOT run in production."
65 );
66 Ok(Arc::new(AllowAllHook::new(sink)))
67 },
68 AuthzMode::Webhook => {
69 let url = authz
70 .hook
71 .url
72 .as_deref()
73 .map(str::trim)
74 .filter(|s| !s.is_empty())
75 .ok_or(AuthzBootstrapError::MissingWebhookUrl)?
76 .to_owned();
77 validate_outbound_url(&url)
78 .map_err(|e| AuthzBootstrapError::InvalidWebhookUrl(e.to_string()))?;
79 let hook = WebhookHook::new(url, Duration::from_millis(authz.hook.timeout_ms), sink)?;
80 Ok(Arc::new(hook))
81 },
82 }
83}
84
85fn build_sink(pool: Option<Arc<sqlx::PgPool>>) -> Arc<dyn AuthzAuditSink> {
86 pool.map_or_else(
87 || -> Arc<dyn AuthzAuditSink> { Arc::new(NullAuditSink) },
88 |p| -> Arc<dyn AuthzAuditSink> {
89 Arc::new(DbAuditSink::new(GovernanceDecisionRepository::from_pool(p)))
90 },
91 )
92}