Skip to main content

systemprompt_security/authz/
rule_based.rs

1//! Core `AuthzDecisionHook` wrapping the in-process [`super::resolver`].
2//!
3//! `RuleBasedHook` is the canonical RBAC layer: it loads
4//! `access_control_rules` for the request's entity, runs the sync resolver
5//! over them, and emits an `AuthzDecision`. Exposed as a hook so extensions
6//! can compose it explicitly with their own ABAC predicates via
7//! [`super::CompositeAuthzHook`]:
8//!
9//! ```ignore
10//! let composite = CompositeAuthzHook::new(vec![
11//!     Arc::new(RuleBasedHook::new(pool.clone(), sink.clone())),
12//!     Arc::new(MyAbacHook::new(...)),
13//! ]);
14//! ```
15//!
16//! Put `RuleBasedHook` first so a coarse-grained RBAC reject short-circuits
17//! the chain before any per-attribute lookup runs.
18
19use 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}