systemprompt_security/authz/
rule_based.rs1use std::sync::Arc;
27
28use async_trait::async_trait;
29use sqlx::PgPool;
30
31use super::audit::{AuthzAuditSink, AuthzSource};
32use super::error::{AuthzError, AuthzResult};
33use super::hook::AuthzDecisionHook;
34use super::parent_chain::{ChainIndexCache, ChainSources, ParentChainIndex, ResolveBase};
35use super::registry::AuthzHookContext;
36use super::repository::AccessControlRepository;
37use super::subject::{
38 SharedSubjectAttributeProvider, SubjectDimension, dimensions_of, discover_subject_providers,
39 gather_subject_attributes,
40};
41use super::types::{AuthzDecision, AuthzRequest, Decision, DenyReason};
42
43#[derive(Clone)]
44pub struct RuleBasedHook {
45 repo: AccessControlRepository,
46 sink: Arc<dyn AuthzAuditSink>,
47 providers: Vec<SharedSubjectAttributeProvider>,
48 dimensions: Vec<SubjectDimension>,
49 sources: Arc<ChainSources>,
50 cache: Arc<ChainIndexCache>,
51}
52
53impl std::fmt::Debug for RuleBasedHook {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("RuleBasedHook")
56 .field("repo", &self.repo)
57 .field("sink", &self.sink)
58 .field("providers", &self.providers)
59 .field("dimensions", &self.dimensions)
60 .finish_non_exhaustive()
61 }
62}
63
64impl RuleBasedHook {
65 #[must_use]
66 pub fn new(pool: Arc<PgPool>, sink: Arc<dyn AuthzAuditSink>, sources: ChainSources) -> Self {
67 let providers = discover_subject_providers(&AuthzHookContext {
68 pool: Arc::clone(&pool),
69 sink: Arc::clone(&sink),
70 });
71 Self {
72 repo: AccessControlRepository::from_pool(pool),
73 sink,
74 dimensions: dimensions_of(&providers),
75 providers,
76 sources: Arc::new(sources),
77 cache: Arc::new(ChainIndexCache::default()),
78 }
79 }
80
81 async fn chain_index(&self) -> AuthzResult<Arc<ParentChainIndex>> {
82 self.cache.get(&self.repo, Arc::clone(&self.sources)).await
83 }
84
85 async fn fault(&self, req: &AuthzRequest, error: &AuthzError) -> AuthzDecision {
89 let policy = AuthzSource::RuleBased.policy().to_owned();
90 let detail = error.to_string();
91 let decision = AuthzDecision::Deny {
92 reason: DenyReason::HookUnavailable {
93 policy: policy.clone(),
94 detail: detail.clone(),
95 },
96 policy,
97 };
98 tracing::warn!(
99 entity = %req.entity,
100 user_id = %req.user_id,
101 error = %detail,
102 "rule-based authz hook fault",
103 );
104 self.sink
105 .record(req, &decision, AuthzSource::RuleBased)
106 .await;
107 decision
108 }
109}
110
111#[async_trait]
112impl AuthzDecisionHook for RuleBasedHook {
113 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
114 let kind = req.entity.kind();
115 let id = req.entity.id_str();
116
117 let entity = match self.repo.get_entity(kind, id).await {
118 Ok(row) => row,
119 Err(err) => return self.fault(&req, &err).await,
120 };
121 let rules = match self.repo.list_rules_for_entity(kind, id).await {
122 Ok(rules) => rules,
123 Err(err) => return self.fault(&req, &err).await,
124 };
125
126 let index = match self.chain_index().await {
127 Ok(index) => index,
128 Err(err) => return self.fault(&req, &err).await,
129 };
130
131 let attributes = gather_subject_attributes(&self.providers, &req.user_id).await;
132 let decision = index.resolve(
133 kind,
134 id,
135 ResolveBase {
136 rules: &rules,
137 user_id: &req.user_id,
138 user_roles: &req.roles,
139 default_included: entity.map(|e| e.default_included),
140 attributes: &attributes,
141 dimensions: &self.dimensions,
142 },
143 );
144
145 let policy = AuthzSource::RuleBased.policy().to_owned();
146 let authz_decision = match decision {
147 Decision::Allow { .. } => AuthzDecision::Allow,
148 Decision::Deny { reason } => AuthzDecision::Deny { reason, policy },
149 Decision::Pending { reason } => {
156 tracing::error!(
157 %reason,
158 "a governance hold reached the rule-based resolver, which cannot park a \
159 request; refusing it"
160 );
161 AuthzDecision::Deny {
162 reason: DenyReason::PolicyViolation {
163 policy: "require_approval".to_owned(),
164 detail: std::borrow::Cow::Borrowed(
165 "approval required, but this enforcement point cannot hold a request",
166 ),
167 },
168 policy,
169 }
170 },
171 };
172 self.sink
173 .record(&req, &authz_decision, AuthzSource::RuleBased)
174 .await;
175 authz_decision
176 }
177}