1616"""
1717
1818import logging
19+ import time
1920
2021from casbin import SyncedEnforcer
2122from casbin_adapter .enforcer import initialize_enforcer
2223from django .conf import settings
24+ from django .core .cache import cache
2325
2426from openedx_authz .engine .adapter import ExtendedAdapter
2527
@@ -62,7 +64,10 @@ class AuthzEnforcer:
6264 Any of the two approaches will yield the same singleton enforcer instance.
6365 """
6466
67+ CACHE_KEY = "authz_policy_last_modified_timestamp"
68+
6569 _enforcer = None
70+ _last_policy_load_timestamp = None
6671
6772 def __new__ (cls ):
6873 """Singleton pattern to ensure a single enforcer instance."""
@@ -141,13 +146,57 @@ def configure_enforcer_auto_save_and_load(cls):
141146 auto_load_policy_interval = getattr (settings , "CASBIN_AUTO_LOAD_POLICY_INTERVAL" , 0 )
142147 auto_save_policy = getattr (settings , "CASBIN_AUTO_SAVE_POLICY" , True )
143148
144- if auto_load_policy_interval > 0 :
145- cls .configure_enforcer_auto_loading (auto_load_policy_interval )
146- else :
147- logger .warning ("CASBIN_AUTO_LOAD_POLICY_INTERVAL is not set or zero; auto-load is disabled." )
149+ # TODO: remove autoload in favor of cache invalidation?
150+ # if auto_load_policy_interval > 0:
151+ # cls.configure_enforcer_auto_loading(auto_load_policy_interval)
152+ # else:
153+ # logger.warning("CASBIN_AUTO_LOAD_POLICY_INTERVAL is not set or zero; auto-load is disabled.")
148154
149155 cls .configure_enforcer_auto_save (auto_save_policy )
150156
157+ @classmethod
158+ def load_policy_if_needed (cls ):
159+ """Load policy if the last load timestamp indicates it's needed.
160+
161+ This method checks if the policy needs to be reloaded comparing
162+ the last load timestamp with the last modified timestamp in cache
163+ and reloads it if necessary.
164+
165+ Returns:
166+ None
167+ """
168+ last_modified_timestamp = cache .get (cls .CACHE_KEY )
169+
170+ current_timestamp = time .time ()
171+
172+ if last_modified_timestamp is None :
173+ # No timestamp in cache; initialize it
174+ cache .set (cls .CACHE_KEY , current_timestamp , None )
175+ logger .info (f">>>> Initialized policy last modified timestamp in cache. { current_timestamp } " )
176+
177+ if (
178+ cls ._last_policy_load_timestamp is None or
179+ last_modified_timestamp > cls ._last_policy_load_timestamp
180+ ):
181+ # Policy has been modified since last load; reload it
182+ cls ._enforcer .load_policy ()
183+ cls ._last_policy_load_timestamp = current_timestamp
184+ logger .info (f">>>> Reloaded policy at { current_timestamp } " )
185+
186+ @classmethod
187+ def invalidate_policy_cache (cls ):
188+ """Invalidate the current policy cache to force a reload on next check.
189+
190+ This method updates the last modified timestamp in the cache to
191+ the current time, indicating that the policy has changed.
192+
193+ Returns:
194+ None
195+ """
196+ current_timestamp = time .time ()
197+ cache .set (cls .CACHE_KEY , current_timestamp , None )
198+ logger .info (f">>>> Invalidated policy cache at { current_timestamp } " )
199+
151200 @classmethod
152201 def get_enforcer (cls ) -> SyncedEnforcer :
153202 """Get the enforcer instance, creating it if needed.
@@ -158,6 +207,9 @@ def get_enforcer(cls) -> SyncedEnforcer:
158207 if cls ._enforcer is None :
159208 cls ._enforcer = cls ._initialize_enforcer ()
160209
210+ # (re)load policy if needed
211+ cls .load_policy_if_needed ()
212+
161213 # HACK: This code block will only be useful when in Ulmo to deactivate
162214 # the enforcer when the new library experience is disabled. It should be
163215 # removed for the next release cycle.
0 commit comments