Skip to content

Commit cbecf01

Browse files
committed
WIP: Experimenting with cache invalidation
1 parent b9414ed commit cbecf01

4 files changed

Lines changed: 67 additions & 9 deletions

File tree

openedx_authz/api/roles.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,14 @@ def assign_role_to_subject_in_scope(subject: SubjectData, role: RoleData, scope:
197197
bool: True if the role was assigned successfully, False otherwise.
198198
"""
199199
enforcer = AuthzEnforcer.get_enforcer()
200-
return enforcer.add_role_for_user_in_domain(
200+
success = enforcer.add_role_for_user_in_domain(
201201
subject.namespaced_key,
202202
role.namespaced_key,
203203
scope.namespaced_key,
204204
)
205+
# Invalidate policy cache to ensure changes are picked up
206+
AuthzEnforcer.invalidate_policy_cache()
207+
return success
205208

206209

207210
def batch_assign_role_to_subjects_in_scope(subjects: list[SubjectData], role: RoleData, scope: ScopeData) -> None:
@@ -227,7 +230,10 @@ def unassign_role_from_subject_in_scope(subject: SubjectData, role: RoleData, sc
227230
bool: True if the role was unassigned successfully, False otherwise.
228231
"""
229232
enforcer = AuthzEnforcer.get_enforcer()
230-
return enforcer.delete_roles_for_user_in_domain(subject.namespaced_key, role.namespaced_key, scope.namespaced_key)
233+
success = enforcer.delete_roles_for_user_in_domain(subject.namespaced_key, role.namespaced_key, scope.namespaced_key)
234+
# Invalidate policy cache to ensure changes are picked up
235+
AuthzEnforcer.invalidate_policy_cache()
236+
return success
231237

232238

233239
def batch_unassign_role_from_subjects_in_scope(subjects: list[SubjectData], role: RoleData, scope: ScopeData) -> None:

openedx_authz/engine/enforcer.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
"""
1717

1818
import logging
19+
import time
1920

2021
from casbin import SyncedEnforcer
2122
from casbin_adapter.enforcer import initialize_enforcer
2223
from django.conf import settings
24+
from django.core.cache import cache
2325

2426
from 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.

openedx_authz/rest_api/v1/permissions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def has_permission(self, request, view) -> bool:
183183
"""
184184
if request.user.is_superuser or request.user.is_staff:
185185
return True
186-
AuthzEnforcer.get_enforcer().load_policy()
186+
# AuthzEnforcer.get_enforcer().load_policy()
187187
return self._get_permission_instance(request).has_permission(request, view)
188188

189189
def has_object_permission(self, request, view, obj) -> bool:
@@ -200,7 +200,7 @@ def has_object_permission(self, request, view, obj) -> bool:
200200
"""
201201
if request.user.is_superuser or request.user.is_staff:
202202
return True
203-
AuthzEnforcer.get_enforcer().load_policy()
203+
# AuthzEnforcer.get_enforcer().load_policy()
204204
return self._get_permission_instance(request).has_object_permission(request, view, obj)
205205

206206

openedx_authz/rest_api/v1/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class PermissionValidationMeView(APIView):
103103
)
104104
def post(self, request: HttpRequest) -> Response:
105105
"""Validate one or more permissions for the authenticated user."""
106-
AuthzEnforcer.get_enforcer().load_policy()
106+
# AuthzEnforcer.get_enforcer().load_policy()
107107

108108
serializer = PermissionValidationSerializer(data=request.data, many=True)
109109
serializer.is_valid(raise_exception=True)

0 commit comments

Comments
 (0)