authx_core/policy/
engine.rs1use 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 #[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 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}