diff --git a/examples/multi_section_model.conf b/examples/multi_section_model.conf new file mode 100644 index 00000000..c76f52e7 --- /dev/null +++ b/examples/multi_section_model.conf @@ -0,0 +1,19 @@ +[request_definition] +r = sub, act, obj +r2 = sub, act + +[policy_definition] +p = sub, act, obj +p2 = sub, act + +[role_definition] +g = _, _ +g2 = _,_ + +[policy_effect] +e = some(where (p.eft == allow)) +e2 = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && g(p.act, r.act) && r.obj == p.obj +m2 = r2.sub == p2.sub && g(p2.act, r2.act) \ No newline at end of file diff --git a/examples/multi_section_policy.csv b/examples/multi_section_policy.csv new file mode 100644 index 00000000..25abdf48 --- /dev/null +++ b/examples/multi_section_policy.csv @@ -0,0 +1,7 @@ +p, alice , admin, project1 +p, bob , user, project2 +p2, james, execute + +g, admin, read +g, admin, write +g, user, read diff --git a/src/cached_enforcer.rs b/src/cached_enforcer.rs index 3ab89b90..94442fd7 100644 --- a/src/cached_enforcer.rs +++ b/src/cached_enforcer.rs @@ -6,6 +6,7 @@ use crate::{ core_api::CoreApi, effector::Effector, emitter::{clear_cache, Event, EventData, EventEmitter}, + enforcer::EnforceContext, enforcer::Enforcer, model::Model, rbac::RoleManager, @@ -71,6 +72,21 @@ impl CachedEnforcer { (authorized, false, indices) }) } + pub(crate) fn private_enforce_with_context( + &self, + ctx: EnforceContext, + rvals: &[Dynamic], + cache_key: u64, + ) -> Result<(bool, bool, Option>)> { + Ok(if let Some(authorized) = self.cache.get(&cache_key) { + (authorized, true, None) + } else { + let (authorized, indices) = + self.enforcer.private_enforce_with_context(ctx, &rvals)?; + self.cache.set(cache_key, authorized); + (authorized, false, indices) + }) + } } #[async_trait] @@ -226,6 +242,44 @@ impl CoreApi for CachedEnforcer { Ok(authorized) } + fn enforce_with_context( + &self, + ctx: EnforceContext, + rvals: ARGS, + ) -> Result { + let cache_key = rvals.cache_key(); + let rvals = rvals.try_into_vec()?; + #[allow(unused_variables)] + let (authorized, cached, indices) = + self.private_enforce_with_context(ctx, &rvals, cache_key)?; + + #[cfg(feature = "logging")] + { + self.enforcer.get_logger().print_enforce_log( + rvals.iter().map(|x| x.to_string()).collect(), + authorized, + cached, + ); + + #[cfg(feature = "explain")] + if let Some(indices) = indices { + let all_rules = get_or_err!(self, "p", ModelError::P, "policy") + .get_policy(); + + let rules: Vec = indices + .into_iter() + .filter_map(|y| { + all_rules.iter().nth(y).map(|x| x.join(", ")) + }) + .collect(); + + self.enforcer.get_logger().print_explain_log(rules); + } + } + + Ok(authorized) + } + #[inline] fn enforce_mut(&mut self, rvals: ARGS) -> Result { self.enforce(rvals) diff --git a/src/config.rs b/src/config.rs index 9566c054..a8a3d211 100644 --- a/src/config.rs +++ b/src/config.rs @@ -151,8 +151,7 @@ impl Config { if section.is_empty() { section = DEFAULT_SECTION.to_owned(); } - let section_value = - self.data.entry(section).or_insert_with(HashMap::new); + let section_value = self.data.entry(section).or_default(); // if key not exists then insert, else update let key_value = section_value.get_mut(&option); diff --git a/src/core_api.rs b/src/core_api.rs index 5fdbb49d..131f1676 100644 --- a/src/core_api.rs +++ b/src/core_api.rs @@ -1,6 +1,7 @@ use crate::{ - Adapter, Effector, EnforceArgs, Event, EventEmitter, Filter, Model, Result, - RoleManager, TryIntoAdapter, TryIntoModel, + enforcer::EnforceContext, Adapter, Effector, EnforceArgs, Event, + EventEmitter, Filter, Model, Result, RoleManager, TryIntoAdapter, + TryIntoModel, }; #[cfg(feature = "watcher")] @@ -64,6 +65,13 @@ pub trait CoreApi: Send + Sync { Self: Sized; fn set_effector(&mut self, e: Box); fn enforce(&self, rvals: ARGS) -> Result + where + Self: Sized; + fn enforce_with_context( + &self, + ctx: EnforceContext, + rvals: ARGS, + ) -> Result where Self: Sized; fn enforce_mut(&mut self, rvals: ARGS) -> Result diff --git a/src/enforcer.rs b/src/enforcer.rs index 504d09c6..8bf4a13a 100644 --- a/src/enforcer.rs +++ b/src/enforcer.rs @@ -5,7 +5,7 @@ use crate::{ effector::{DefaultEffector, EffectKind, Effector}, emitter::{Event, EventData, EventEmitter}, error::{ModelError, PolicyError, RequestError}, - get_or_err, + get_or_err, get_or_err_with_context, management_api::MgmtApi, model::{FunctionMap, Model}, rbac::{DefaultRoleManager, RoleManager}, @@ -74,9 +74,33 @@ pub struct Enforcer { logger: Box, } +pub struct EnforceContext { + pub r_type: String, + pub p_type: String, + pub e_type: String, + pub m_type: String, +} + +impl EnforceContext { + pub fn new(suffix: &str) -> Self { + Self { + r_type: format!("r{}", suffix), + p_type: format!("p{}", suffix), + e_type: format!("e{}", suffix), + m_type: format!("m{}", suffix), + } + } + pub fn get_cache_key(&self) -> String { + format!( + "EnforceContext{{{}-{}-{}-{}}}", + &self.r_type, &self.p_type, &self.e_type, &self.m_type, + ) + } +} + impl EventEmitter for Enforcer { fn on(&mut self, e: Event, f: fn(&mut Self, EventData)) { - self.events.entry(e).or_insert_with(Vec::new).push(f) + self.events.entry(e).or_default().push(f) } fn off(&mut self, e: Event) { @@ -127,7 +151,136 @@ impl Enforcer { self.eft.new_stream(&e_ast.value, max(policy_len, 1)); let m_ast_compiled = self .engine - .compile_expression(&escape_eval(&m_ast.value)) + .compile_expression(escape_eval(&m_ast.value)) + .map_err(Into::>::into)?; + + if policy_len == 0 { + for token in p_ast.tokens.iter() { + scope.push_constant(token, String::new()); + } + + let eval_result = self + .engine + .eval_ast_with_scope::(&mut scope, &m_ast_compiled)?; + let eft = if eval_result { + EffectKind::Allow + } else { + EffectKind::Indeterminate + }; + + eft_stream.push_effect(eft); + + return Ok((eft_stream.next(), None)); + } + + for pvals in policies { + scope.rewind(scope_len); + + if p_ast.tokens.len() != pvals.len() { + return Err(PolicyError::UnmatchPolicyDefinition( + p_ast.tokens.len(), + pvals.len(), + ) + .into()); + } + for (ptoken, pval) in p_ast.tokens.iter().zip(pvals.iter()) { + scope.push_constant(ptoken, pval.to_owned()); + } + + let eval_result = self + .engine + .eval_ast_with_scope::(&mut scope, &m_ast_compiled)?; + let eft = match p_ast.tokens.iter().position(|x| x == "p_eft") { + Some(j) if eval_result => { + let p_eft = &pvals[j]; + if p_eft == "deny" { + EffectKind::Deny + } else if p_eft == "allow" { + EffectKind::Allow + } else { + EffectKind::Indeterminate + } + } + None if eval_result => EffectKind::Allow, + _ => EffectKind::Indeterminate, + }; + + if eft_stream.push_effect(eft) { + break; + } + } + + Ok((eft_stream.next(), { + #[cfg(feature = "explain")] + { + eft_stream.explain() + } + #[cfg(not(feature = "explain"))] + { + None + } + })) + } + + pub(crate) fn private_enforce_with_context( + &self, + ctx: EnforceContext, + rvals: &[Dynamic], + ) -> Result<(bool, Option>)> { + if !self.enabled { + return Ok((true, None)); + } + + let mut scope: Scope = Scope::new(); + let r_ast = get_or_err_with_context!( + self, + "r", + &ctx.r_type, + ModelError::R, + "request" + ); + let p_ast = get_or_err_with_context!( + self, + "p", + &ctx.p_type, + ModelError::P, + "policy" + ); + let m_ast = get_or_err_with_context!( + self, + "m", + &ctx.m_type, + ModelError::M, + "matcher" + ); + let e_ast = get_or_err_with_context!( + self, + "e", + &ctx.e_type, + ModelError::E, + "effector" + ); + + if r_ast.tokens.len() != rvals.len() { + return Err(RequestError::UnmatchRequestDefinition( + r_ast.tokens.len(), + rvals.len(), + ) + .into()); + } + + for (rtoken, rval) in r_ast.tokens.iter().zip(rvals.iter()) { + scope.push_constant_dynamic(rtoken, rval.to_owned()); + } + + let policies = p_ast.get_policy(); + let (policy_len, scope_len) = (policies.len(), scope.len()); + + let mut eft_stream = + self.eft.new_stream(&e_ast.value, max(policy_len, 1)); + let m_ast_compiled = self + .engine + .compile_expression(escape_eval(&m_ast.value)) .map_err(Into::>::into)?; if policy_len == 0 { @@ -426,6 +579,75 @@ impl CoreApi for Enforcer { Ok(authorized) } + /// Enforce decides whether a "subject" can access a "object" with the operation "action", + /// input parameters are usually: (sub, obj, act). + /// this function will add suffix to each model eg. r2, p2, e2, m2, g2, + /// + /// # Examples + /// ``` + /// use casbin::prelude::*; + /// use casbin::EnforceContext; + /// + /// #[cfg(feature = "runtime-async-std")] + /// #[async_std::main] + /// async fn main() -> Result<()> { + /// let mut e = Enforcer::new("examples/multi_section_model.conf", "examples/multi_section_policy.csv").await?; + /// assert_eq!(true, e.enforce(("alice", "read", "project1"))?); + /// let ctx = EnforceContext::new("2"); + /// assert_eq!(true, e.enforce_with_context(ctx, ("james", "execute"))?); + /// Ok(()) + /// } + /// + /// #[cfg(feature = "runtime-tokio")] + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// let mut e = Enforcer::new("examples/multi_section_model.conf", "examples/multi_section_policy.csv").await?; + /// assert_eq!(true, e.enforce(("alice", "read", "project1"))?); + /// let ctx = EnforceContext::new("2"); + /// assert_eq!(true, e.enforce_with_context(ctx, ("james", "execute"))?); + /// + /// Ok(()) + /// } + /// #[cfg(all(not(feature = "runtime-async-std"), not(feature = "runtime-tokio")))] + /// fn main() {} + /// ``` + + fn enforce_with_context( + &self, + ctx: EnforceContext, + rvals: ARGS, + ) -> Result { + let rvals = rvals.try_into_vec()?; + #[allow(unused_variables)] + let (authorized, indices) = + self.private_enforce_with_context(ctx, &rvals)?; + + #[cfg(feature = "logging")] + { + self.logger.print_enforce_log( + rvals.iter().map(|x| x.to_string()).collect(), + authorized, + false, + ); + + #[cfg(feature = "explain")] + if let Some(indices) = indices { + let all_rules = get_or_err!(self, "p", ModelError::P, "policy") + .get_policy(); + + let rules: Vec = indices + .into_iter() + .filter_map(|y| { + all_rules.iter().nth(y).map(|x| x.join(", ")) + }) + .collect(); + + self.logger.print_explain_log(rules); + } + } + + Ok(authorized) + } fn enforce_mut(&mut self, rvals: ARGS) -> Result { self.enforce(rvals) diff --git a/src/lib.rs b/src/lib.rs index 3dae380d..01006207 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,7 @@ pub use effector::{ DefaultEffectStream, DefaultEffector, EffectKind, Effector, EffectorStream, }; pub use emitter::{Event, EventData, EventEmitter, EventKey}; +pub use enforcer::EnforceContext; pub use enforcer::Enforcer; pub use error::Error; pub use internal_api::InternalApi; diff --git a/src/macros.rs b/src/macros.rs index 48836041..ed33f2ce 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -21,6 +21,29 @@ macro_rules! get_or_err { }}; } +#[macro_export] +macro_rules! get_or_err_with_context { + ($this:ident, $key:expr, $ctx:expr, $err:expr, $msg:expr) => {{ + $this + .get_model() + .get_model() + .get($key) + .ok_or_else(|| { + $crate::error::Error::from($err(format!( + "Missing {} definition in conf file", + $msg + ))) + })? + .get($ctx) + .ok_or_else(|| { + $crate::error::Error::from($err(format!( + "Missing {} section in conf file", + $msg + ))) + })? + }}; +} + #[macro_export] macro_rules! register_g_function { ($enforcer:ident, $fname:ident, $ast:ident) => {{ diff --git a/src/rbac/default_role_manager.rs b/src/rbac/default_role_manager.rs index 3e632c58..e61bee31 100644 --- a/src/rbac/default_role_manager.rs +++ b/src/rbac/default_role_manager.rs @@ -53,15 +53,12 @@ impl DefaultRoleManager { ) -> NodeIndex { let domain = domain.unwrap_or(DEFAULT_DOMAIN); - let graph = self - .all_domains - .entry(domain.into()) - .or_insert_with(StableDiGraph::new); + let graph = self.all_domains.entry(domain.into()).or_default(); let role_entry = self .all_domains_indices .entry(domain.into()) - .or_insert_with(HashMap::new) + .or_default() .entry(name.into()); let vacant_entry = match role_entry {