diff --git a/casbin/__init__.py b/casbin/__init__.py index fa812588..0d3bb4db 100644 --- a/casbin/__init__.py +++ b/casbin/__init__.py @@ -17,6 +17,9 @@ from .distributed_enforcer import DistributedEnforcer from .fast_enforcer import FastEnforcer from .async_enforcer import AsyncEnforcer +from .cached_enforcer import CachedEnforcer +from .synced_cached_enforcer import SyncedCachedEnforcer +from .async_cached_enforcer import AsyncCachedEnforcer from . import util from .persist import * from .effect import * diff --git a/casbin/async_cached_enforcer.py b/casbin/async_cached_enforcer.py new file mode 100644 index 00000000..a8322864 --- /dev/null +++ b/casbin/async_cached_enforcer.py @@ -0,0 +1,97 @@ +from casbin.cache import Cache, ErrNoSuchKey +from casbin.cache.default_cache import DefaultCache +from casbin.async_enforcer import AsyncEnforcer + + +class AsyncCachedEnforcer(AsyncEnforcer): + def __init__(self, *args, **kwargs): + self._enable_cache = True + self._cache = DefaultCache() + self._expire_time = None + super().__init__(*args, **kwargs) + + def enable_cache(self, enable_cache): + self._enable_cache = enable_cache + + def set_expire_time(self, expire_time): + self._expire_time = expire_time + + def set_cache(self, cache): + self._cache = cache + + def invalidate_cache(self): + self._cache.clear() + + @staticmethod + def get_cache_key(*params): + parts = [] + for p in params: + if isinstance(p, str): + parts.append(p) + elif hasattr(p, "get_cache_key"): + parts.append(p.get_cache_key()) + else: + return None + parts.append("$$") + return "".join(parts) + + def enforce(self, *rvals): + if not self._enable_cache: + return super().enforce(*rvals) + + key = self.get_cache_key(*rvals) + if key is None: + return super().enforce(*rvals) + + try: + return self._cache.get(key) + except ErrNoSuchKey: + pass + + result = super().enforce(*rvals) + self._cache.set(key, result, self._expire_time) + return result + + async def load_policy(self): + self.invalidate_cache() + return await super().load_policy() + + async def clear_policy(self): + self.invalidate_cache() + return await super().clear_policy() + + async def _add_policy(self, sec, ptype, rule): + self.invalidate_cache() + return await super()._add_policy(sec, ptype, rule) + + async def _add_policies(self, sec, ptype, rules): + self.invalidate_cache() + return await super()._add_policies(sec, ptype, rules) + + async def _update_policy(self, sec, ptype, old_rule, new_rule): + self.invalidate_cache() + return await super()._update_policy(sec, ptype, old_rule, new_rule) + + async def _update_policies(self, sec, ptype, old_rules, new_rules): + self.invalidate_cache() + return await super()._update_policies(sec, ptype, old_rules, new_rules) + + async def _update_filtered_policies(self, sec, ptype, new_rules, field_index, *field_values): + self.invalidate_cache() + return await super()._update_filtered_policies(sec, ptype, new_rules, field_index, *field_values) + + async def _remove_policy(self, sec, ptype, rule): + self.invalidate_cache() + return await super()._remove_policy(sec, ptype, rule) + + async def _remove_policies(self, sec, ptype, rules): + self.invalidate_cache() + return await super()._remove_policies(sec, ptype, rules) + + async def _remove_filtered_policy(self, sec, ptype, field_index, *field_values): + self.invalidate_cache() + return await super()._remove_filtered_policy(sec, ptype, field_index, *field_values) + + async def _remove_filtered_policy_returns_effects(self, sec, ptype, field_index, *field_values): + self.invalidate_cache() + return await super()._remove_filtered_policy_returns_effects(sec, ptype, field_index, *field_values) diff --git a/casbin/cache/__init__.py b/casbin/cache/__init__.py new file mode 100644 index 00000000..8183d799 --- /dev/null +++ b/casbin/cache/__init__.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod + + +class ErrNoSuchKey(LookupError): + pass + + +class Cache(ABC): + @abstractmethod + def set(self, key, value, *extra): + pass + + @abstractmethod + def get(self, key): + pass + + @abstractmethod + def delete(self, key): + pass + + @abstractmethod + def clear(self): + pass diff --git a/casbin/cache/default_cache.py b/casbin/cache/default_cache.py new file mode 100644 index 00000000..07db2d36 --- /dev/null +++ b/casbin/cache/default_cache.py @@ -0,0 +1,28 @@ +import time + +from casbin.cache import Cache, ErrNoSuchKey + + +class DefaultCache(Cache): + def __init__(self): + self._store = {} + + def set(self, key, value, *extra): + ttl = extra[0] if extra else None + expires_at = time.time() + ttl if ttl and ttl > 0 else None + self._store[key] = (value, expires_at) + + def get(self, key): + if key not in self._store: + raise ErrNoSuchKey(key) + value, expires_at = self._store[key] + if expires_at and time.time() > expires_at: + del self._store[key] + raise ErrNoSuchKey(key) + return value + + def delete(self, key): + self._store.pop(key, None) + + def clear(self): + self._store.clear() diff --git a/casbin/cached_enforcer.py b/casbin/cached_enforcer.py new file mode 100644 index 00000000..71bc2954 --- /dev/null +++ b/casbin/cached_enforcer.py @@ -0,0 +1,101 @@ +from casbin.cache import Cache, ErrNoSuchKey +from casbin.cache.default_cache import DefaultCache +from casbin.enforcer import Enforcer + + +class CachedEnforcer(Enforcer): + def __init__(self, *args, **kwargs): + self._enable_cache = True + self._cache = DefaultCache() + self._expire_time = None + super().__init__(*args, **kwargs) + + def enable_cache(self, enable_cache): + self._enable_cache = enable_cache + + def set_expire_time(self, expire_time): + self._expire_time = expire_time + + def set_cache(self, cache): + self._cache = cache + + def invalidate_cache(self): + self._cache.clear() + + @staticmethod + def get_cache_key(*params): + parts = [] + for p in params: + if isinstance(p, str): + parts.append(p) + elif hasattr(p, "get_cache_key"): + parts.append(p.get_cache_key()) + else: + return None + parts.append("$$") + return "".join(parts) + + def enforce(self, *rvals): + if not self._enable_cache: + return super().enforce(*rvals) + + key = self.get_cache_key(*rvals) + if key is None: + return super().enforce(*rvals) + + try: + return self._cache.get(key) + except ErrNoSuchKey: + pass + + result = super().enforce(*rvals) + self._cache.set(key, result, self._expire_time) + return result + + def load_policy(self): + self.invalidate_cache() + return super().load_policy() + + def clear_policy(self): + self.invalidate_cache() + return super().clear_policy() + + def _add_policy(self, sec, ptype, rule): + self.invalidate_cache() + return super()._add_policy(sec, ptype, rule) + + def _add_policies(self, sec, ptype, rules): + self.invalidate_cache() + return super()._add_policies(sec, ptype, rules) + + def _add_policies_ex(self, sec, ptype, rules): + self.invalidate_cache() + return super()._add_policies_ex(sec, ptype, rules) + + def _update_policy(self, sec, ptype, old_rule, new_rule): + self.invalidate_cache() + return super()._update_policy(sec, ptype, old_rule, new_rule) + + def _update_policies(self, sec, ptype, old_rules, new_rules): + self.invalidate_cache() + return super()._update_policies(sec, ptype, old_rules, new_rules) + + def _update_filtered_policies(self, sec, ptype, new_rules, field_index, *field_values): + self.invalidate_cache() + return super()._update_filtered_policies(sec, ptype, new_rules, field_index, *field_values) + + def _remove_policy(self, sec, ptype, rule): + self.invalidate_cache() + return super()._remove_policy(sec, ptype, rule) + + def _remove_policies(self, sec, ptype, rules): + self.invalidate_cache() + return super()._remove_policies(sec, ptype, rules) + + def _remove_filtered_policy(self, sec, ptype, field_index, *field_values): + self.invalidate_cache() + return super()._remove_filtered_policy(sec, ptype, field_index, *field_values) + + def _remove_filtered_policy_returns_effects(self, sec, ptype, field_index, *field_values): + self.invalidate_cache() + return super()._remove_filtered_policy_returns_effects(sec, ptype, field_index, *field_values) diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py index 5b5e0e5c..d1c80d9f 100644 --- a/casbin/core_enforcer.py +++ b/casbin/core_enforcer.py @@ -313,6 +313,13 @@ def enable_auto_notify_watcher(self, auto_notify_watcher): """controls whether to save a policy rule automatically notify the watcher when it is added or removed.""" self.auto_notify_watcher = auto_notify_watcher + def enable_g_function_cache(self, enabled): + """controls whether to cache g() function results.""" + for rm in self.rm_map.values(): + rm.enable_g_cache(enabled) + for crm in self.cond_rm_map.values(): + crm.enable_g_cache(enabled) + def build_role_links(self): """manually rebuild the role inheritance relations.""" diff --git a/casbin/rbac/default_role_manager/role_manager.py b/casbin/rbac/default_role_manager/role_manager.py index 5ea0605b..d6f8c120 100644 --- a/casbin/rbac/default_role_manager/role_manager.py +++ b/casbin/rbac/default_role_manager/role_manager.py @@ -15,6 +15,7 @@ import logging from collections import namedtuple from enum import Enum +from functools import lru_cache from casbin.rbac import RoleManager as RM from casbin.rbac import ConditionalRoleManager as CRM @@ -103,6 +104,8 @@ def __init__(self, max_hierarchy_level=10): self.domain_matching_func = None self.all_links = list() self.all_roles = dict() + self._g_cache_enabled = True + self._g_cache = lru_cache(maxsize=None)(self._g_cached_has_link) def _rebuild(self): self.all_roles = dict() @@ -146,11 +149,26 @@ def add_matching_func(self, fn): def add_domain_matching_func(self, fn=None): self.domain_matching_func = fn + def clear_g_cache(self): + self._g_cache.cache_clear() + + def enable_g_cache(self, enabled): + self._g_cache_enabled = enabled + if not enabled: + self.clear_g_cache() + + def _g_cached_has_link(self, name1, name2, domain): + user = self._get_role(name1) + role = self._get_role(name2) + return self._has_link(name2, [user], self.max_hierarchy_level) + def clear(self): + self.clear_g_cache() self.all_roles = dict() self.all_links = list() def add_link(self, name1, name2, *domain): + self.clear_g_cache() self.all_links.append(Link(name1, name2)) user = self._get_role(name1) @@ -168,6 +186,7 @@ def add_link(self, name1, name2, *domain): def delete_link(self, name1, name2, *domain): if Link(name1, name2) not in self.all_links: return + self.clear_g_cache() self.all_links.remove(Link(name1, name2)) user = self._get_role(name1) @@ -181,6 +200,10 @@ def delete_link(self, name1, name2, *domain): role.remove_role(r) def has_link(self, name1, name2, *domain): + if self._g_cache_enabled: + d = domain[0] if domain else None + return self._g_cache(name1, name2, d) + user = self._get_role(name1) role = self._get_role(name2) @@ -226,6 +249,7 @@ def __init__(self, max_hierarchy_level=10): self.matching_func = None self.domain_matching_func = None self.matching_func = lambda name1, name2: name1 == name2 + self._g_cache_enabled = True def add_matching_func(self, fn): self.matching_func = fn @@ -233,6 +257,12 @@ def add_matching_func(self, fn): def add_domain_matching_func(self, fn=None): self.domain_matching_func = fn + def enable_g_cache(self, enabled): + self._g_cache_enabled = enabled + + def clear_g_cache(self): + pass + def _get_domain(self, *domain): if len(domain) > 1: raise RuntimeError("error: domain should be 1 parameter") @@ -303,10 +333,17 @@ def __init__(self, max_hierarchy_level=10): def _rebuild(self): self.rm_map = dict() + def enable_g_cache(self, enabled): + super().enable_g_cache(enabled) + for rm in self.rm_map.values(): + rm.enable_g_cache(enabled) + def _get_role_manager(self, *domain): domain1 = self._get_domain(*domain) if domain1 not in self.rm_map: - self.rm_map[domain1] = super()._get_role_manager(*domain) + rm = super()._get_role_manager(*domain) + rm._g_cache_enabled = self._g_cache_enabled + self.rm_map[domain1] = rm return self.rm_map[domain1] @@ -370,8 +407,29 @@ def match_error_handler(fn, key1, key2): class ConditionalRoleManager(RoleManager, CRM): + def __init__(self, max_hierarchy_level=10): + super().__init__(max_hierarchy_level) + self._g_cache_cond = lru_cache(maxsize=None)(self._g_cached_cond_has_link) + + def clear_g_cache(self): + super().clear_g_cache() + self._g_cache_cond.cache_clear() + + def _g_cached_cond_has_link(self, name1, name2, domain): + if name1 == name2 or (self.matching_func is not None and self._matching_fn(name1, name2)): + return True + user = self._get_role(name1) + role = self._get_role(name2) + if domain is None: + return self._has_link(role.name, [user], self.max_hierarchy_level) + return self._has_link(role.name, [user], self.max_hierarchy_level, domain) + def has_link(self, name1, name2, *domains): """determines whether role: name1 inherits role: name2.""" + if self._g_cache_enabled: + d = domains[0] if domains else None + return self._g_cache_cond(name1, name2, d) + if name1 == name2 or (self.matching_func is not None and self._matching_fn(name1, name2)): return True @@ -481,6 +539,7 @@ def _get_conditional_role_manager(self, *domain, store=False): if rm is None: rm = ConditionalRoleManager(max_hierarchy_level=self.max_hierarchy_level) + rm._g_cache_enabled = self._g_cache_enabled if store: self.rm_map[domain1] = rm if self.domain_matching_func is not None: diff --git a/casbin/synced_cached_enforcer.py b/casbin/synced_cached_enforcer.py new file mode 100644 index 00000000..a5986176 --- /dev/null +++ b/casbin/synced_cached_enforcer.py @@ -0,0 +1,32 @@ +from casbin.synced_enforcer import SyncedEnforcer, RWLockWrite, AtomicBool +from casbin.cached_enforcer import CachedEnforcer + + +class SyncedCachedEnforcer(SyncedEnforcer): + def __init__(self, model=None, adapter=None): + self._e = CachedEnforcer(model, adapter) + self._rwlock = RWLockWrite() + self._rl = self._rwlock.gen_rlock() + self._wl = self._rwlock.gen_wlock() + self._auto_loading = AtomicBool(False) + self._auto_loading_thread = None + + def enable_cache(self, enable): + with self._wl: + self._e.enable_cache(enable) + + def enable_g_function_cache(self, enabled): + with self._wl: + self._e.enable_g_function_cache(enabled) + + def invalidate_cache(self): + with self._wl: + self._e.invalidate_cache() + + def set_expire_time(self, expire_time): + with self._wl: + self._e.set_expire_time(expire_time) + + def set_cache(self, cache): + with self._wl: + self._e.set_cache(cache) diff --git a/tests/test_cached_enforcer.py b/tests/test_cached_enforcer.py new file mode 100644 index 00000000..9d235327 --- /dev/null +++ b/tests/test_cached_enforcer.py @@ -0,0 +1,289 @@ +import os +from unittest import TestCase, IsolatedAsyncioTestCase + +import casbin +from casbin.cache.default_cache import DefaultCache + + +def get_examples(path): + examples_path = os.path.split(os.path.realpath(__file__))[0] + "/../examples/" + return os.path.abspath(examples_path + path) + + +class TestCachedEnforcer(TestCase): + def test_cache_basic(self): + e = casbin.CachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + e.remove_policy("alice", "data1", "read") + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + def test_cache_rbac(self): + e = casbin.CachedEnforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + self.assertTrue(e.enforce("alice", "data2", "read")) + self.assertTrue(e.enforce("alice", "data2", "write")) + + e.remove_policies([["alice", "data1", "read"], ["bob", "data2", "write"]]) + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("bob", "data2", "write")) + self.assertTrue(e.enforce("alice", "data2", "read")) + self.assertTrue(e.enforce("alice", "data2", "write")) + + def test_cache_clear_policy(self): + e = casbin.CachedEnforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("bob", "data2", "write")) + + e.clear_policy() + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("bob", "data2", "write")) + + def test_cache_invalidate(self): + e = casbin.CachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.invalidate_cache() + + e.remove_policy("alice", "data1", "read") + self.assertFalse(e.enforce("alice", "data1", "read")) + + def test_cache_add_clears(self): + e = casbin.CachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertFalse(e.enforce("alice", "data2", "read")) + e.add_policy("alice", "data2", "read") + self.assertTrue(e.enforce("alice", "data2", "read")) + + def test_cache_enable_disable(self): + e = casbin.CachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.enable_cache(False) + e.remove_policy("alice", "data1", "read") + self.assertFalse(e.enforce("alice", "data1", "read")) + + def test_cache_set_custom(self): + e = casbin.CachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + e.set_cache(DefaultCache()) + self.assertTrue(e.enforce("alice", "data1", "read")) + + def test_cache_get_key(self): + key = casbin.CachedEnforcer.get_cache_key("alice", "data1", "read") + self.assertEqual(key, "alice$$data1$$read$$") + + +class TestGFunctionCache(TestCase): + def test_g_cache_basic(self): + e = casbin.Enforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + e.enable_g_function_cache(True) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("alice", "data2", "read")) + + def test_g_cache_clear_on_add(self): + e = casbin.Enforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + e.enable_g_function_cache(True) + + self.assertFalse(e.enforce("bob", "data2", "read")) + e.add_grouping_policy("bob", "data2_admin") + self.assertTrue(e.enforce("bob", "data2", "read")) + + def test_g_cache_clear_on_remove(self): + e = casbin.Enforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + e.enable_g_function_cache(True) + + self.assertTrue(e.enforce("alice", "data2", "read")) + e.remove_grouping_policy("alice", "data2_admin") + self.assertFalse(e.enforce("alice", "data2", "read")) + + def test_g_cache_disable(self): + e = casbin.Enforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + e.enable_g_function_cache(False) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("alice", "data2", "read")) + + def test_g_cache_with_domains(self): + e = casbin.Enforcer( + get_examples("rbac_with_domains_model.conf"), + get_examples("rbac_with_domains_policy.csv"), + ) + e.enable_g_function_cache(True) + + self.assertTrue(e.enforce("alice", "domain1", "data1", "read")) + self.assertFalse(e.enforce("alice", "domain1", "data2", "read")) + + def test_g_cache_clear_on_load_policy(self): + e = casbin.Enforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + e.enable_g_function_cache(True) + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.load_policy() + self.assertTrue(e.enforce("alice", "data1", "read")) + + +class TestSyncedCachedEnforcer(TestCase): + def test_synced_cache_basic(self): + e = casbin.SyncedCachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + e.remove_policy("alice", "data1", "read") + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + def test_synced_cache_invalidate(self): + e = casbin.SyncedCachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.invalidate_cache() + e.remove_policy("alice", "data1", "read") + self.assertFalse(e.enforce("alice", "data1", "read")) + + def test_synced_enable_cache(self): + e = casbin.SyncedCachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.enable_cache(False) + e.remove_policy("alice", "data1", "read") + self.assertFalse(e.enforce("alice", "data1", "read")) + + def test_synced_g_function_cache(self): + e = casbin.SyncedCachedEnforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + e.enable_g_function_cache(True) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("bob", "data1", "read")) + + e.add_grouping_policy("bob", "data2_admin") + self.assertTrue(e.enforce("bob", "data2", "read")) + + +class TestAsyncCachedEnforcer(IsolatedAsyncioTestCase): + async def test_async_cache_basic(self): + e = casbin.AsyncCachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + await e.load_policy() + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + await e.remove_policy("alice", "data1", "read") + + self.assertFalse(e.enforce("alice", "data1", "read")) + self.assertFalse(e.enforce("alice", "data1", "write")) + self.assertFalse(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("alice", "data2", "write")) + + async def test_async_cache_invalidate(self): + e = casbin.AsyncCachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + await e.load_policy() + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.invalidate_cache() + await e.remove_policy("alice", "data1", "read") + self.assertFalse(e.enforce("alice", "data1", "read")) + + async def test_async_enable_cache(self): + e = casbin.AsyncCachedEnforcer( + get_examples("basic_model.conf"), + get_examples("basic_policy.csv"), + ) + await e.load_policy() + + self.assertTrue(e.enforce("alice", "data1", "read")) + e.enable_cache(False) + await e.remove_policy("alice", "data1", "read") + self.assertFalse(e.enforce("alice", "data1", "read")) + + async def test_async_g_function_cache(self): + e = casbin.AsyncCachedEnforcer( + get_examples("rbac_model.conf"), + get_examples("rbac_policy.csv"), + ) + await e.load_policy() + e.enable_g_function_cache(True) + + self.assertTrue(e.enforce("alice", "data1", "read")) + self.assertTrue(e.enforce("alice", "data2", "read")) + self.assertFalse(e.enforce("bob", "data1", "read")) + + await e.add_grouping_policy("bob", "data2_admin") + self.assertTrue(e.enforce("bob", "data2", "read"))