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::repository::AccessControlRepository;
30use super::resolver::{ResolveInput, resolve};
31use super::types::{AuthzDecision, AuthzRequest, Decision, DenyReason};
32
33#[derive(Debug, Clone)]
34pub struct RuleBasedHook {
35 repo: AccessControlRepository,
36 sink: Arc<dyn AuthzAuditSink>,
37}
38
39impl RuleBasedHook {
40 #[must_use]
41 pub fn new(pool: Arc<PgPool>, sink: Arc<dyn AuthzAuditSink>) -> Self {
42 Self {
43 repo: AccessControlRepository::from_pool(pool),
44 sink,
45 }
46 }
47
48 async fn fault(&self, req: &AuthzRequest, detail: &str) -> AuthzDecision {
49 let policy = AuthzSource::RuleBased.policy().to_owned();
50 let decision = AuthzDecision::Deny {
51 reason: DenyReason::HookUnavailable {
52 policy: policy.clone(),
53 },
54 policy,
55 };
56 tracing::warn!(
57 entity = %req.entity,
58 user_id = %req.user_id,
59 error = %detail,
60 "rule-based authz hook fault",
61 );
62 self.sink
63 .record(req, &decision, AuthzSource::RuleBased)
64 .await;
65 decision
66 }
67}
68
69#[async_trait]
70impl AuthzDecisionHook for RuleBasedHook {
71 async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
72 let kind = req.entity.kind();
73 let id = req.entity.id_str();
74
75 let entity = match self.repo.get_entity(kind, id).await {
76 Ok(row) => row,
77 Err(err) => return self.fault(&req, &err.to_string()).await,
78 };
79 let rules = match self.repo.list_rules_for_entity(kind, id).await {
80 Ok(rules) => rules,
81 Err(err) => return self.fault(&req, &err.to_string()).await,
82 };
83
84 let decision = resolve(ResolveInput {
85 entity: &req.entity,
86 rules: &rules,
87 user_id: &req.user_id,
88 user_roles: &req.roles,
89 default_included: entity.map(|e| e.default_included),
90 parents: &[],
91 });
92
93 let policy = AuthzSource::RuleBased.policy().to_owned();
94 let authz_decision = match decision {
95 Decision::Allow { .. } => AuthzDecision::Allow,
96 Decision::Deny { reason } => AuthzDecision::Deny { reason, policy },
97 };
98 self.sink
99 .record(&req, &authz_decision, AuthzSource::RuleBased)
100 .await;
101 authz_decision
102 }
103}