systemprompt_security/authz/
runtime.rs1use std::sync::{Arc, OnceLock, RwLock};
23use std::time::Duration;
24
25use systemprompt_models::profile::{AuthzMode, GovernanceConfig, UNRESTRICTED_ACKNOWLEDGEMENT};
26
27use super::audit::{AuthzAuditSink, DbAuditSink, GovernanceDecisionRepository, NullAuditSink};
28use super::error::{AuthzBootstrapError, AuthzResult};
29use super::hook::{AllowAllHook, AuthzDecisionHook, DenyAllHook, WebhookHook};
30
31type SharedHook = Arc<dyn AuthzDecisionHook>;
32
33fn slot() -> &'static RwLock<Option<SharedHook>> {
34 static SLOT: OnceLock<RwLock<Option<SharedHook>>> = OnceLock::new();
35 SLOT.get_or_init(|| RwLock::new(None))
36}
37
38pub fn install_global_hook(hook: SharedHook) {
39 if let Ok(mut guard) = slot().write() {
40 *guard = Some(hook);
41 }
42}
43
44pub fn clear_global_hook() {
45 if let Ok(mut guard) = slot().write() {
46 *guard = None;
47 }
48}
49
50#[must_use]
51pub fn global_hook() -> Option<SharedHook> {
52 slot().read().ok().and_then(|g| g.clone())
53}
54
55pub fn install_from_governance_config(
56 governance: Option<&GovernanceConfig>,
57 pool: Option<Arc<sqlx::PgPool>>,
58) -> AuthzResult<()> {
59 let sink = build_sink(pool);
60
61 let Some(authz) = governance.and_then(|g| g.authz.as_ref()) else {
62 tracing::error!(
63 "governance.authz block missing — installing DenyAllHook (all requests will be denied)"
64 );
65 install_global_hook(Arc::new(DenyAllHook::new(sink)));
66 return Ok(());
67 };
68
69 match authz.hook.mode {
70 AuthzMode::Disabled => {
71 tracing::warn!("governance.authz.hook.mode = disabled — all requests will be denied");
72 install_global_hook(Arc::new(DenyAllHook::new(sink)));
73 Ok(())
74 },
75 AuthzMode::Unrestricted => {
76 let ack = authz.hook.acknowledgement.as_deref().map_or("", str::trim);
77 if ack != UNRESTRICTED_ACKNOWLEDGEMENT {
78 return Err(AuthzBootstrapError::MissingUnrestrictedAcknowledgement {
79 expected: UNRESTRICTED_ACKNOWLEDGEMENT,
80 }
81 .into());
82 }
83 tracing::error!(
84 "governance.authz.hook.mode = unrestricted — ALL REQUESTS WILL BE ALLOWED. This \
85 MUST NOT run in production."
86 );
87 install_global_hook(Arc::new(AllowAllHook::new(sink)));
88 Ok(())
89 },
90 AuthzMode::Webhook => {
91 let url = authz
92 .hook
93 .url
94 .as_deref()
95 .map(str::trim)
96 .filter(|s| !s.is_empty())
97 .ok_or(AuthzBootstrapError::MissingWebhookUrl)?
98 .to_owned();
99 let hook = WebhookHook::new(url, Duration::from_millis(authz.hook.timeout_ms), sink)?;
100 install_global_hook(Arc::new(hook));
101 Ok(())
102 },
103 }
104}
105
106fn build_sink(pool: Option<Arc<sqlx::PgPool>>) -> Arc<dyn AuthzAuditSink> {
107 pool.map_or_else(
108 || -> Arc<dyn AuthzAuditSink> { Arc::new(NullAuditSink) },
109 |p| -> Arc<dyn AuthzAuditSink> {
110 Arc::new(DbAuditSink::new(GovernanceDecisionRepository::from_pool(p)))
111 },
112 )
113}