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