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]
53 pub fn new(pool: Arc<PgPool>, sink: Arc<dyn AuthzAuditSink>) -> Self {
54 let providers = discover_subject_providers(&AuthzHookContext {
55 pool: Arc::clone(&pool),
56 sink: Arc::clone(&sink),
57 });
58 Self {
59 repo: AccessControlRepository::from_pool(pool),
60 sink,
61 dimensions: dimensions_of(&providers),
62 providers,
63 }
64 }
65
66 async fn fault(&self, req: &AuthzRequest, detail: &str) -> AuthzDecision {
67 let policy = AuthzSource::RuleBased.policy().to_owned();
68 let decision = AuthzDecision::Deny {
69 reason: DenyReason::HookUnavailable {
70 policy: policy.clone(),
71 },
72 policy,
73 };
74 tracing::warn!(
75 entity = %req.entity,
76 user_id = %req.user_id,
77 error = %detail,
78 "rule-based authz hook fault",
79 );
80 self.sink
81 .record(req, &decision, AuthzSource::RuleBased)
82 .await;
83 decision
84 }
85}
86
87#[async_trait]
88impl AuthzDecisionHook for RuleBasedHook {
89 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
90 let kind = req.entity.kind();
91 let id = req.entity.id_str();
92
93 let entity = match self.repo.get_entity(kind, id).await {
94 Ok(row) => row,
95 Err(err) => return self.fault(&req, &err.to_string()).await,
96 };
97 let rules = match self.repo.list_rules_for_entity(kind, id).await {
98 Ok(rules) => rules,
99 Err(err) => return self.fault(&req, &err.to_string()).await,
100 };
101
102 let attributes = gather_subject_attributes(&self.providers, &req.user_id).await;
103 let decision = resolve(ResolveInput {
104 entity: &req.entity,
105 rules: &rules,
106 user_id: &req.user_id,
107 user_roles: &req.roles,
108 default_included: entity.map(|e| e.default_included),
109 parents: &[],
110 attributes: &attributes,
111 dimensions: &self.dimensions,
112 });
113
114 let policy = AuthzSource::RuleBased.policy().to_owned();
115 let authz_decision = match decision {
116 Decision::Allow { .. } => AuthzDecision::Allow,
117 Decision::Deny { reason } => AuthzDecision::Deny { reason, policy },
118 };
119 self.sink
120 .record(&req, &authz_decision, AuthzSource::RuleBased)
121 .await;
122 authz_decision
123 }
124}