This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new ab8b1e9  fix: key caches by exact request instead of 64-bit hash
ab8b1e9 is described below

commit ab8b1e937fa5f3c093fdff23dee07800a893adf1
Author: Yang Luo <[email protected]>
AuthorDate: Fri Oct 9 22:52:57 2026 +0800

    fix: key caches by exact request instead of 64-bit hash
---
 src/cached_enforcer.rs           | 167 +++++++++++++++++++++++++++++----------
 src/emitter.rs                   |   2 +-
 src/rbac/default_role_manager.rs |  31 ++++----
 3 files changed, 144 insertions(+), 56 deletions(-)

diff --git a/src/cached_enforcer.rs b/src/cached_enforcer.rs
index ddb03da..260052a 100644
--- a/src/cached_enforcer.rs
+++ b/src/cached_enforcer.rs
@@ -27,19 +27,15 @@ use crate::{error::ModelError, get_or_err};
 
 use async_trait::async_trait;
 use parking_lot::RwLock;
-use rhai::Dynamic;
+use rhai::{Array, Blob, Dynamic, ImmutableString, Map};
 
-use std::{
-    collections::{hash_map::DefaultHasher, HashMap},
-    hash::{Hash, Hasher},
-    sync::Arc,
-};
+use std::{collections::HashMap, fmt::Write, sync::Arc};
 
 type EventCallback = fn(&mut CachedEnforcer, EventData);
 
 pub struct CachedEnforcer {
     enforcer: Enforcer,
-    cache: Box<dyn Cache<u64, bool>>,
+    cache: Box<dyn Cache<String, bool>>,
     events: HashMap<Event, Vec<EventCallback>>,
 }
 
@@ -61,36 +57,100 @@ impl EventEmitter<Event> for CachedEnforcer {
     }
 }
 
+// Encode the request without loss, so that two different requests can never
+// share a cache entry. Values that cannot be encoded are not cached.
+fn cache_key(prefix: &str, rvals: &[Dynamic]) -> Option<String> {
+    let mut key = String::new();
+    write_str(&mut key, prefix);
+    write_values(&mut key, rvals)?;
+    Some(key)
+}
+
+fn write_str(key: &mut String, s: &str) {
+    let _ = write!(key, "s{}:{}", s.len(), s);
+}
+
+fn write_values(key: &mut String, values: &[Dynamic]) -> Option<()> {
+    let _ = write!(key, "a{}:", values.len());
+    values.iter().try_for_each(|v| write_value(key, v))
+}
+
+fn write_value(key: &mut String, value: &Dynamic) -> Option<()> {
+    if value.is_unit() {
+        key.push('u');
+    } else if let Ok(b) = value.as_bool() {
+        key.push(if b { 't' } else { 'f' });
+    } else if let Ok(i) = value.as_int() {
+        let _ = write!(key, "i{};", i);
+    } else if let Ok(c) = value.as_char() {
+        let _ = write!(key, "c{};", c as u32);
+    } else if let Some(s) = value.read_lock::<ImmutableString>() {
+        write_str(key, &s);
+    } else if let Some(a) = value.read_lock::<Array>() {
+        write_values(key, &a)?;
+    } else if let Some(m) = value.read_lock::<Map>() {
+        let _ = write!(key, "m{}:", m.len());
+        for (k, v) in m.iter() {
+            write_str(key, k);
+            write_value(key, v)?;
+        }
+    } else {
+        let b = value.read_lock::<Blob>()?;
+        let _ = write!(key, "x{}:", b.len());
+        b.iter().for_each(|byte| {
+            let _ = write!(key, "{:02x}", byte);
+        });
+    }
+
+    Some(())
+}
+
 impl CachedEnforcer {
     #[inline]
     fn invalidate_cache(&self) {
         self.cache.clear();
     }
 
+    fn cached_enforce(
+        &self,
+        cache_key: Option<String>,
+        enforce: impl FnOnce() -> Result<(bool, Option<Vec<usize>>)>,
+    ) -> Result<(bool, bool, Option<Vec<usize>>)> {
+        let cache_key = match cache_key {
+            Some(cache_key) => cache_key,
+            None => {
+                let (authorized, indices) = enforce()?;
+                return Ok((authorized, false, indices));
+            }
+        };
+
+        Ok(if let Some(authorized) = self.cache.get(&cache_key) {
+            (authorized, true, None)
+        } else {
+            let (authorized, indices) = enforce()?;
+            self.cache.set(cache_key, authorized);
+            (authorized, false, indices)
+        })
+    }
+
     pub(crate) fn private_enforce(
         &self,
         rvals: &[Dynamic],
-        cache_key: u64,
     ) -> Result<(bool, bool, Option<Vec<usize>>)> {
         if !self.enforcer.is_enabled() {
             let (authorized, indices) = self.enforcer.private_enforce(rvals)?;
             return Ok((authorized, false, indices));
         }
 
-        Ok(if let Some(authorized) = self.cache.get(&cache_key) {
-            (authorized, true, None)
-        } else {
-            let (authorized, indices) =
-                self.enforcer.private_enforce(&rvals)?;
-            self.cache.set(cache_key, authorized);
-            (authorized, false, indices)
+        self.cached_enforce(cache_key("", rvals), || {
+            self.enforcer.private_enforce(rvals)
         })
     }
+
     pub(crate) fn private_enforce_with_context(
         &self,
         ctx: EnforceContext,
         rvals: &[Dynamic],
-        cache_key: u64,
     ) -> Result<(bool, bool, Option<Vec<usize>>)> {
         if !self.enforcer.is_enabled() {
             let (authorized, indices) =
@@ -98,20 +158,8 @@ impl CachedEnforcer {
             return Ok((authorized, false, indices));
         }
 
-        let cache_key = {
-            let mut hasher = DefaultHasher::new();
-            cache_key.hash(&mut hasher);
-            ctx.get_cache_key().hash(&mut hasher);
-            hasher.finish()
-        };
-
-        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)
+        self.cached_enforce(cache_key(&ctx.get_cache_key(), rvals), || {
+            self.enforcer.private_enforce_with_context(ctx, rvals)
         })
     }
 }
@@ -260,11 +308,9 @@ impl CoreApi for CachedEnforcer {
     }
 
     fn enforce<ARGS: EnforceArgs>(&self, rvals: ARGS) -> Result<bool> {
-        let cache_key = rvals.cache_key();
         let rvals = rvals.try_into_vec()?;
         #[allow(unused_variables)]
-        let (authorized, cached, indices) =
-            self.private_enforce(&rvals, cache_key)?;
+        let (authorized, cached, indices) = self.private_enforce(&rvals)?;
 
         #[cfg(feature = "logging")]
         {
@@ -298,11 +344,10 @@ impl CoreApi for CachedEnforcer {
         ctx: EnforceContext,
         rvals: ARGS,
     ) -> Result<bool> {
-        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)?;
+            self.private_enforce_with_context(ctx, &rvals)?;
 
         #[cfg(feature = "logging")]
         {
@@ -341,11 +386,9 @@ impl CoreApi for CachedEnforcer {
         &self,
         rvals: ARGS,
     ) -> Result<(bool, Vec<Vec<String>>)> {
-        let cache_key = rvals.cache_key();
         let rvals = rvals.try_into_vec()?;
         #[allow(unused_variables)]
-        let (authorized, cached, indices) =
-            self.private_enforce(&rvals, cache_key)?;
+        let (authorized, cached, indices) = self.private_enforce(&rvals)?;
 
         let rules = match indices {
             Some(indices) => {
@@ -460,12 +503,12 @@ impl CoreApi for CachedEnforcer {
     }
 }
 
-impl CachedApi<u64, bool> for CachedEnforcer {
-    fn get_mut_cache(&mut self) -> &mut dyn Cache<u64, bool> {
+impl CachedApi<String, bool> for CachedEnforcer {
+    fn get_mut_cache(&mut self) -> &mut dyn Cache<String, bool> {
         &mut *self.cache
     }
 
-    fn set_cache(&mut self, cache: Box<dyn Cache<u64, bool>>) {
+    fn set_cache(&mut self, cache: Box<dyn Cache<String, bool>>) {
         self.cache = cache;
     }
 }
@@ -482,6 +525,20 @@ mod tests {
         true
     }
 
+    #[test]
+    fn test_cache_key_is_exact() {
+        let key = |v: Vec<Dynamic>| cache_key("", &v);
+
+        assert_ne!(
+            key(vec!["ab".into(), "c".into()]),
+            key(vec!["a".into(), "bc".into()])
+        );
+        assert_ne!(key(vec!["1".into()]), key(vec![1.into()]));
+        assert_ne!(key(vec!["a".into()]), key(vec!['a'.into()]));
+        assert_ne!(cache_key("2", &[]), cache_key("", &["2".into()]));
+        assert_eq!(key(vec![Dynamic::from(1_u8)]), None);
+    }
+
     #[test]
     fn test_send_sync() {
         assert!(is_send::<CachedEnforcer>());
@@ -573,4 +630,32 @@ m2 = r2.sub == p2.sub && r2.act == p2.act
             .enforce_with_context(EnforceContext::new("2"), ("alice", "read"))
             .unwrap());
     }
+
+    #[cfg(not(target_arch = "wasm32"))]
+    #[cfg_attr(
+        all(feature = "runtime-async-std", not(target_arch = "wasm32")),
+        async_std::test
+    )]
+    #[cfg_attr(
+        all(feature = "runtime-tokio", not(target_arch = "wasm32")),
+        tokio::test
+    )]
+    async fn test_cache_key_hash_collision() {
+        use crate::{MemoryAdapter, MgmtApi};
+
+        let granted = "c80525cd02b185f6";
+        let denied = "b80d2770d0241ce9";
+
+        let mut e = CachedEnforcer::new(
+            "examples/basic_model.conf",
+            MemoryAdapter::default(),
+        )
+        .await
+        .unwrap();
+        e.add_policy(vec!["alice".into(), granted.into(), "read".into()])
+            .await
+            .unwrap();
+        assert!(e.enforce(("alice", granted, "read")).unwrap());
+        assert!(!e.enforce(("alice", denied, "read")).unwrap());
+    }
 }
diff --git a/src/emitter.rs b/src/emitter.rs
index 8c7b914..de623c3 100644
--- a/src/emitter.rs
+++ b/src/emitter.rs
@@ -103,7 +103,7 @@ pub(crate) fn notify_logger_and_watcher<T: CoreApi>(e: &mut 
T, d: EventData) {
 
 #[cfg(feature = "cached")]
 #[allow(unused_variables)]
-pub(crate) fn clear_cache<T: CoreApi + CachedApi<u64, bool>>(
+pub(crate) fn clear_cache<T: CoreApi + CachedApi<String, bool>>(
     ce: &mut T,
     d: EventData,
 ) {
diff --git a/src/rbac/default_role_manager.rs b/src/rbac/default_role_manager.rs
index c9aac3d..d0ce364 100644
--- a/src/rbac/default_role_manager.rs
+++ b/src/rbac/default_role_manager.rs
@@ -9,12 +9,6 @@ use std::collections::{hash_map::Entry, HashMap, HashSet};
 #[cfg(feature = "cached")]
 use crate::cache::{Cache, DefaultCache};
 
-#[cfg(feature = "cached")]
-use std::{
-    collections::hash_map::DefaultHasher,
-    hash::{Hash, Hasher},
-};
-
 const DEFAULT_DOMAIN: &str = "DEFAULT";
 
 pub struct DefaultRoleManager {
@@ -22,7 +16,7 @@ pub struct DefaultRoleManager {
     all_domains_indices: HashMap<String, HashMap<String, NodeIndex<u32>>>,
     explicit_links: HashMap<String, HashSet<(String, String)>>,
     #[cfg(feature = "cached")]
-    cache: DefaultCache<u64, bool>,
+    cache: DefaultCache<(String, String, String), bool>,
     max_hierarchy_level: usize,
     role_matching_fn: Option<MatchingFn>,
     domain_matching_fn: Option<MatchingFn>,
@@ -484,13 +478,11 @@ impl RoleManager for DefaultRoleManager {
         }
 
         #[cfg(feature = "cached")]
-        let cache_key = {
-            let mut hasher = DefaultHasher::new();
-            name1.hash(&mut hasher);
-            name2.hash(&mut hasher);
-            domain.unwrap_or(DEFAULT_DOMAIN).hash(&mut hasher);
-            hasher.finish()
-        };
+        let cache_key = (
+            name1.to_owned(),
+            name2.to_owned(),
+            domain.unwrap_or(DEFAULT_DOMAIN).to_owned(),
+        );
 
         #[cfg(feature = "cached")]
         if let Some(res) = self.cache.get(&cache_key) {
@@ -807,6 +799,17 @@ mod tests {
         v
     }
 
+    #[test]
+    fn test_has_link_cache_hash_collision() {
+        let authorized = "u85b1cd3ad675c9e1";
+        let unauthorized = "ua03255daeb17d9fb";
+
+        let mut rm = DefaultRoleManager::new(10);
+        rm.add_link(authorized, "admin", None);
+        assert!(rm.has_link(authorized, "admin", None));
+        assert!(!rm.has_link(unauthorized, "admin", None));
+    }
+
     #[test]
     fn test_role() {
         let mut rm = DefaultRoleManager::new(3);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to