Skip to main content

authx_core/policy/
engine.rs

1use async_trait::async_trait;
2use tracing::instrument;
3
4use crate::error::{AuthError, Result};
5use crate::identity::Identity;
6
7#[derive(Debug, Clone)]
8pub struct AuthzContext<'a> {
9    pub action: &'a str,
10    pub identity: &'a Identity,
11    pub resource_id: Option<&'a str>,
12}
13
14#[derive(Debug, PartialEq, Eq)]
15pub enum PolicyDecision {
16    Allow,
17    Deny,
18    Abstain,
19}
20
21#[async_trait]
22pub trait Policy: Send + Sync + 'static {
23    fn name(&self) -> &'static str;
24    async fn evaluate(&self, ctx: &AuthzContext<'_>) -> PolicyDecision;
25}
26
27pub struct AuthzEngine {
28    policies: Vec<Box<dyn Policy>>,
29}
30
31impl AuthzEngine {
32    pub fn new() -> Self {
33        Self {
34            policies: Vec::new(),
35        }
36    }
37
38    pub fn add_policy(&mut self, policy: impl Policy) {
39        self.policies.push(Box::new(policy));
40    }
41
42    /// Enforces authorization. Raises [`AuthError::Forbidden`] if denied.
43    ///
44    /// Evaluation order:
45    /// 1. Walk policies in registration order.
46    /// 2. First explicit Allow → permit.
47    /// 3. First explicit Deny  → reject.
48    /// 4. All Abstain          → fall through to RBAC check.
49    #[instrument(skip(self, identity), fields(action, user_id = %identity.user.id))]
50    pub async fn enforce(
51        &self,
52        action: &str,
53        identity: &Identity,
54        resource_id: Option<&str>,
55    ) -> Result<()> {
56        let ctx = AuthzContext {
57            action,
58            identity,
59            resource_id,
60        };
61
62        for policy in &self.policies {
63            match policy.evaluate(&ctx).await {
64                PolicyDecision::Allow => {
65                    tracing::debug!(action, policy = policy.name(), "policy allow");
66                    return Ok(());
67                }
68                PolicyDecision::Deny => {
69                    tracing::warn!(action, policy = policy.name(), "policy deny");
70                    return Err(AuthError::Forbidden(format!(
71                        "action '{action}' denied by policy '{}'",
72                        policy.name()
73                    )));
74                }
75                PolicyDecision::Abstain => {}
76            }
77        }
78
79        // Default: RBAC from org membership
80        if identity.has_permission(action) {
81            tracing::debug!(action, "rbac allow");
82            Ok(())
83        } else {
84            tracing::warn!(action, "rbac deny");
85            Err(AuthError::Forbidden(format!(
86                "action '{action}' not permitted for current role"
87            )))
88        }
89    }
90}
91
92impl Default for AuthzEngine {
93    fn default() -> Self {
94        Self::new()
95    }
96}