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