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