Skip to content

Commit 5f03619

Browse files
committed
squash!: Changed implementation to use UUID for cache invalidation
1 parent 8da767b commit 5f03619

6 files changed

Lines changed: 81 additions & 77 deletions

File tree

openedx_authz/engine/enforcer.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"""
1717

1818
import logging
19-
import time
19+
from uuid import uuid4
2020

2121
from casbin import SyncedEnforcer
2222
from casbin_adapter.enforcer import initialize_enforcer
@@ -70,7 +70,7 @@ class AuthzEnforcer:
7070

7171
_enforcer = None
7272
_adapter = None
73-
_last_policy_load_timestamp = None
73+
_last_policy_loaded_version = None
7474

7575
def __new__(cls):
7676
"""Singleton pattern to ensure a single enforcer instance."""
@@ -158,43 +158,42 @@ def configure_enforcer_auto_save_and_load(cls):
158158

159159
@classmethod
160160
def load_policy_if_needed(cls):
161-
"""Load policy if the last load timestamp indicates it's needed.
161+
"""Load policy if the last load version indicates it's needed.
162162
163163
This method checks if the policy needs to be reloaded comparing
164-
the last load timestamp with the last modified timestamp in cache
164+
the last load version with the version in the cache invalidation model,
165165
and reloads it if necessary.
166166
167167
Returns:
168168
None
169169
"""
170-
last_modified_timestamp = PolicyCacheControl.get_last_modified_timestamp()
170+
last_version = PolicyCacheControl.get_version()
171171

172-
current_timestamp = time.time()
173-
174-
if last_modified_timestamp is None:
172+
if last_version is None:
175173
# No timestamp in cache; initialize it
176-
PolicyCacheControl.set_last_modified_timestamp(current_timestamp)
177-
logger.info("Initialized policy last modified timestamp in cache control.")
174+
last_version = uuid4()
175+
PolicyCacheControl.set_version(last_version)
176+
logger.info("Initialized policy last modified version in cache control.")
178177

179-
if cls._last_policy_load_timestamp is None or last_modified_timestamp > cls._last_policy_load_timestamp:
178+
if cls._last_policy_loaded_version is None or last_version != cls._last_policy_loaded_version:
180179
# Policy has been modified since last load; reload it
181180
cls._enforcer.load_policy()
182-
cls._last_policy_load_timestamp = current_timestamp
183-
logger.info(f"Reloaded policy at {current_timestamp}")
181+
cls._last_policy_loaded_version = last_version
182+
logger.info(f"Reloaded policy to version {last_version}")
184183

185184
@classmethod
186185
def invalidate_policy_cache(cls):
187186
"""Invalidate the current policy cache to force a reload on next check.
188187
189-
This method updates the last modified timestamp in the cache to
190-
the current time, indicating that the policy has changed.
188+
This method updates the last modified version in the cache invalidation model
189+
to a new UUID, indicating that the policy has changed.
191190
192191
Returns:
193192
None
194193
"""
195-
current_timestamp = time.time()
196-
PolicyCacheControl.set_last_modified_timestamp(current_timestamp)
197-
logger.info(f"Invalidated policy cache at {current_timestamp}")
194+
new_version = uuid4()
195+
PolicyCacheControl.set_version(new_version)
196+
logger.info(f"Invalidated policy cache to version {new_version}")
198197

199198
@classmethod
200199
def get_enforcer(cls) -> SyncedEnforcer:

openedx_authz/migrations/0005_policycachecontrol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Generated by Django 4.2.24 on 2025-11-13 22:32
1+
# Generated by Django 4.2.24 on 2025-11-14 22:38
22

3-
import datetime
3+
import uuid
44

55
from django.db import migrations, models
66

@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
1515
name="PolicyCacheControl",
1616
fields=[
1717
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
18-
("last_modified", models.DateTimeField(default=datetime.datetime.now)),
18+
("version", models.UUIDField(default=uuid.uuid4)),
1919
],
2020
),
2121
]

openedx_authz/models/engine.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Models for the authorization engine."""
22

3-
from datetime import datetime
3+
from uuid import UUID, uuid4
44

55
from django.db import models
66

@@ -9,14 +9,14 @@ class PolicyCacheControl(models.Model):
99
"""Model to control policy cache invalidation.
1010
1111
This model can be used to trigger cache invalidation for authorization policies
12-
by updating its timestamp. Whenever this model is updated, the authorization
12+
by changing the version. Whenever this model is updated, the authorization
1313
engine should invalidate its cached policies.
1414
"""
1515

16-
last_modified = models.DateTimeField(default=datetime.now)
16+
version = models.UUIDField(default=uuid4)
1717

1818
def save(self, *args, **kwargs):
19-
"""Override save to update the timestamp."""
19+
"""Override save to ensure a single instance."""
2020
self.pk = 1 # Ensure a single instance
2121
super().save(*args, **kwargs)
2222

@@ -27,23 +27,23 @@ def get(cls):
2727
return obj
2828

2929
@classmethod
30-
def get_last_modified_timestamp(cls):
31-
"""Get the last modified timestamp for policy cache control.
30+
def get_version(cls):
31+
"""Get the version for policy cache control.
3232
3333
Returns:
34-
float: The timestamp of the last update.
34+
UUID: The version of the last update.
3535
"""
3636
instance = cls.get()
37-
return instance.last_modified.timestamp()
37+
return instance.version
3838

3939
@classmethod
40-
def set_last_modified_timestamp(cls, timestamp: float):
41-
"""Update the last modified timestamp to the current time.
40+
def set_version(cls, version: UUID):
41+
"""Update the cache version.
4242
43-
This method updates the timestamp, which can be used to signal
43+
This method updates the cache version, which can be used to signal
4444
that the policy cache should be invalidated.
4545
"""
4646
instance = cls.get()
47-
instance.last_modified = datetime.fromtimestamp(timestamp)
47+
instance.version = version
4848

4949
instance.save()

openedx_authz/tests/api/test_roles.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,6 @@ def test_assign_role_creates_extended_casbin_rule(self):
978978
self.assertIn(subject_data.namespaced_key, extended_rule.casbin_rule_key)
979979
self.assertIn(scope_data.namespaced_key, extended_rule.casbin_rule_key)
980980

981-
982981
@ddt_data(
983982
# Test user with single role in single scope
984983
("alice", ["lib:Org1:math_101"], {"library_admin"}),

openedx_authz/tests/test_enforcer.py

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import time
99
from unittest.mock import patch
10+
from uuid import uuid4
1011

1112
import casbin
1213
from ddt import data as ddt_data
@@ -839,19 +840,22 @@ def test_load_policy_if_needed_initializes_cache_timestamp(self, mock_toggle):
839840
"""Test that load_policy_if_needed initializes cache timestamp on first call.
840841
841842
Expected result:
842-
- On first call, cache invalidation key is set with current timestamp
843-
- Policy is loaded since last load timestamp is None
843+
- On first call, cache invalidation model is initialized
844+
- Policy is loaded since last load version is None
844845
"""
845846
mock_toggle.return_value = True
846847

847-
AuthzEnforcer._last_policy_load_timestamp = None # pylint: disable=protected-access
848-
848+
AuthzEnforcer._last_policy_loaded_version = None # pylint: disable=protected-access
849849
# get_enforcer calls load_policy_if_needed internally
850850
AuthzEnforcer.get_enforcer()
851851

852-
cached_timestamp = PolicyCacheControl.get_last_modified_timestamp()
853-
self.assertIsNotNone(cached_timestamp)
854-
self.assertIsNotNone(AuthzEnforcer._last_policy_load_timestamp) # pylint: disable=protected-access
852+
cached_version = PolicyCacheControl.get_version()
853+
self.assertIsNotNone(cached_version)
854+
self.assertIsNotNone(AuthzEnforcer._last_policy_loaded_version) # pylint: disable=protected-access
855+
self.assertEqual(
856+
AuthzEnforcer._last_policy_loaded_version, # pylint: disable=protected-access
857+
cached_version,
858+
)
855859

856860
@patch("openedx_authz.engine.enforcer.libraries_v2_enabled")
857861
@override_settings(CASBIN_AUTO_LOAD_POLICY_INTERVAL=0)
@@ -860,25 +864,25 @@ def test_load_policy_if_needed_loads_when_stale(self, mock_toggle):
860864
861865
Expected result:
862866
- If policy is stale, it is reloaded
863-
- _last_policy_load_timestamp is updated with new timestamp
867+
- _last_policy_loaded_version is updated with new version
864868
"""
865869
mock_toggle.return_value = True
866870

867-
now = time.time()
868-
stale_timestamp = now - 60 # 60 seconds ago
871+
stale_version = uuid4()
872+
current_version = uuid4()
869873

870-
# Set last load timestamp to stale value
871-
AuthzEnforcer._last_policy_load_timestamp = stale_timestamp # pylint: disable=protected-access
872-
# Set last cache invalidation to a more recent time
873-
PolicyCacheControl.set_last_modified_timestamp(now)
874+
# Set last loaded version to stale value
875+
AuthzEnforcer._last_policy_loaded_version = stale_version # pylint: disable=protected-access
876+
# Set last cache invalidation current version
877+
PolicyCacheControl.set_version(current_version)
874878

875879
# get_enforcer calls load_policy_if_needed internally
876880
AuthzEnforcer.get_enforcer()
877881

878-
self.assertIsNotNone(AuthzEnforcer._last_policy_load_timestamp) # pylint: disable=protected-access
879-
self.assertGreater(
880-
AuthzEnforcer._last_policy_load_timestamp, # pylint: disable=protected-access
881-
stale_timestamp,
882+
self.assertIsNotNone(AuthzEnforcer._last_policy_loaded_version) # pylint: disable=protected-access
883+
self.assertEqual(
884+
AuthzEnforcer._last_policy_loaded_version, # pylint: disable=protected-access
885+
current_version,
882886
)
883887

884888
@patch("openedx_authz.engine.enforcer.libraries_v2_enabled")
@@ -888,41 +892,41 @@ def test_load_policy_if_needed_doesnt_reload_when_not_stale(self, mock_toggle):
888892
889893
Expected result:
890894
- If policy is not stale, it is not reloaded
891-
- _last_policy_load_timestamp remains unchanged
895+
- _last_policy_loaded_version remains unchanged
892896
"""
893897
mock_toggle.return_value = True
894898

895-
now = time.time()
899+
current_version = uuid4()
896900

897-
# Set last load timestamp to current time
898-
AuthzEnforcer._last_policy_load_timestamp = now # pylint: disable=protected-access
899-
# Set last cache invalidation to an earlier time
900-
PolicyCacheControl.set_last_modified_timestamp(now - 60) # 60 seconds ago
901+
# Set last loaded version to current version
902+
AuthzEnforcer._last_policy_loaded_version = current_version # pylint: disable=protected-access
903+
# Set last cache invalidation to same version
904+
PolicyCacheControl.set_version(current_version)
901905

902906
# get_enforcer calls load_policy_if_needed internally
903907
AuthzEnforcer.get_enforcer()
904908

905909
self.assertEqual(
906-
AuthzEnforcer._last_policy_load_timestamp, # pylint: disable=protected-access
907-
now,
910+
AuthzEnforcer._last_policy_loaded_version, # pylint: disable=protected-access
911+
current_version,
908912
)
909913

910914
@patch("openedx_authz.engine.enforcer.libraries_v2_enabled")
911915
@override_settings(CASBIN_AUTO_LOAD_POLICY_INTERVAL=0)
912916
def test_invalidate_policy_cache(self, mock_toggle):
913-
"""Test that invalidate_policy_cache updates the cache invalidation key.
917+
"""Test that invalidate_policy_cache updates the cache invalidation model.
914918
915919
Expected result:
916-
- Cache invalidation key is updated with current timestamp
920+
- Cache invalidation key is updated to a new version
917921
"""
918922
mock_toggle.return_value = True
919923

920-
AuthzEnforcer._last_policy_load_timestamp = time.time() # pylint: disable=protected-access
921-
old_cache_value = time.time() - 60 # 60 seconds ago
922-
PolicyCacheControl.set_last_modified_timestamp(old_cache_value)
924+
AuthzEnforcer._last_policy_loaded_version = uuid4() # pylint: disable=protected-access
925+
old_cache_value = uuid4()
926+
PolicyCacheControl.set_version(old_cache_value)
923927

924928
AuthzEnforcer.invalidate_policy_cache()
925929

926-
new_cache_value = PolicyCacheControl.get_last_modified_timestamp()
930+
new_cache_value = PolicyCacheControl.get_version()
927931
self.assertIsNotNone(new_cache_value)
928-
self.assertGreater(new_cache_value, old_cache_value)
932+
self.assertNotEqual(new_cache_value, old_cache_value)

openedx_authz/tests/test_models.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
which run against the real ContentLibrary model.
1414
"""
1515

16+
from uuid import UUID, uuid4
17+
1618
from casbin_adapter.models import CasbinRule
1719
from django.contrib.auth import get_user_model
1820
from django.test import TestCase
@@ -142,21 +144,21 @@ def test_extended_casbin_rule_cascade_deletion_when_subject_deleted(self):
142144
class TestPolicyCacheControlModel(TestCase):
143145
"""Test cases for the PolicyCacheControl model."""
144146

145-
def test_get_and_set_last_modified_timestamp(self):
146-
"""Test getting and setting the last modified timestamp.
147+
def test_get_and_set_version(self):
148+
"""Test getting and setting the cache version.
147149
148150
Expected Result:
149-
- Initially, the timestamp is set to the current time.
150-
- After setting a new timestamp, it reflects the updated value.
151+
- Initially, the version is set to a UUID.
152+
- After setting a new version, it reflects the updated value.
151153
"""
152-
initial_timestamp = PolicyCacheControl.get_last_modified_timestamp()
153-
self.assertIsInstance(initial_timestamp, float)
154+
initial_version = PolicyCacheControl.get_version()
155+
self.assertIsInstance(initial_version, UUID)
154156

155-
new_timestamp = initial_timestamp + 1000.0 # Simulate a future timestamp
156-
PolicyCacheControl.set_last_modified_timestamp(new_timestamp)
157+
new_version = uuid4() # Generate a new UUID
158+
PolicyCacheControl.set_version(new_version)
157159

158-
updated_timestamp = PolicyCacheControl.get_last_modified_timestamp()
159-
self.assertEqual(updated_timestamp, new_timestamp)
160+
updated_version = PolicyCacheControl.get_version()
161+
self.assertEqual(updated_version, new_version)
160162

161163
def test_singleton_behavior(self):
162164
"""Test that only one instance of PolicyCacheControl exists.

0 commit comments

Comments
 (0)