systemprompt_security/authz/
rule_based.rs1use std::sync::Arc;
23
24use async_trait::async_trait;
25use sqlx::PgPool;
26
27use super::audit::{AuthzAuditSink, AuthzSource};
28use super::hook::AuthzDecisionHook;
29use super::registry::AuthzHookContext;
30use super::repository::AccessControlRepository;
31use super::resolver::{ResolveInput, resolve};
32use super::subject::{
33 SharedSubjectAttributeProvider, SubjectDimension, dimensions_of, discover_subject_providers,
34 gather_subject_attributes,
35};
36use super::types::{AuthzDecision, AuthzRequest, Decision, DenyReason};
37
38#[derive(Debug, Clone)]
39pub struct RuleBasedHook {
40 repo: AccessControlRepository,
41 sink: Arc<dyn AuthzAuditSink>,
42 providers: Vec<SharedSubjectAttributeProvider>,
43 dimensions: Vec<SubjectDimension>,
44}
45
46impl RuleBasedHook {
47 #[must_use]
48 pub fn new(pool: Arc<PgPool>, sink: Arc<dyn AuthzAuditSink>) -> Self {
49 let providers = discover_subject_providers(&AuthzHookContext {
50 pool: Arc::clone(&pool),
51 sink: Arc::clone(&sink),
52 });
53 Self {
54 repo: AccessControlRepository::from_pool(pool),
55 sink,
56 dimensions: dimensions_of(&providers),
57 providers,
58 }
59 }
60
61 async fn fault(&self, req: &AuthzRequest, detail: &str) -> AuthzDecision {
62 let policy = AuthzSource::RuleBased.policy().to_owned();
63 let decision = AuthzDecision::Deny {
64 reason: DenyReason::HookUnavailable {
65 policy: policy.clone(),
66 },
67 policy,
68 };
69 tracing::warn!(
70 entity = %req.entity,
71 user_id = %req.user_id,
72 error = %detail,
73 "rule-based authz hook fault",
74 );
75 self.sink
76 .record(req, &decision, AuthzSource::RuleBased)
77 .await;
78 decision
79 }
80}
81
82#[async_trait]
83impl AuthzDecisionHook for RuleBasedHook {
84 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
85 let kind = req.entity.kind();
86 let id = req.entity.id_str();
87
88 let entity = match self.repo.get_entity(kind, id).await {
89 Ok(row) => row,
90 Err(err) => return self.fault(&req, &err.to_string()).await,
91 };
92 let rules = match self.repo.list_rules_for_entity(kind, id).await {
93 Ok(rules) => rules,
94 Err(err) => return self.fault(&req, &err.to_string()).await,
95 };
96
97 let attributes = gather_subject_attributes(&self.providers, &req.user_id).await;
98 let decision = resolve(ResolveInput {
99 entity: &req.entity,
100 rules: &rules,
101 user_id: &req.user_id,
102 user_roles: &req.roles,
103 default_included: entity.map(|e| e.default_included),
104 parents: &[],
105 attributes: &attributes,
106 dimensions: &self.dimensions,
107 });
108
109 let policy = AuthzSource::RuleBased.policy().to_owned();
110 let authz_decision = match decision {
111 Decision::Allow { .. } => AuthzDecision::Allow,
112 Decision::Deny { reason } => AuthzDecision::Deny { reason, policy },
113 };
114 self.sink
115 .record(&req, &authz_decision, AuthzSource::RuleBased)
116 .await;
117 authz_decision
118 }
119}