Skip to content

Commit 8d8b893

Browse files
test: add test suite for enforcer based on policy lifecycle
1 parent 87b70a3 commit 8d8b893

1 file changed

Lines changed: 123 additions & 62 deletions

File tree

openedx_authz/tests/test_enforcer.py

Lines changed: 123 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
that would be used in production environments.
66
"""
77

8-
from unittest import TestCase
8+
from django.test import TestCase
99

1010
import casbin
11-
from ddt import data as test_data
11+
from ddt import data as ddt_data
1212
from ddt import ddt, unpack
1313

1414
from openedx_authz.engine.enforcer import enforcer as global_enforcer
@@ -19,6 +19,44 @@
1919
class PolicyLoadingTestSetupMixin(TestCase):
2020
"""Mixin providing policy loading test utilities."""
2121

22+
@staticmethod
23+
def _count_policies_in_file(scope_pattern: str = None, role: str = None):
24+
"""Count policies in the authz.policy file matching the given criteria.
25+
26+
This provides a dynamic way to get expected policy counts without
27+
hardcoding values that might change as the policy file evolves.
28+
29+
Args:
30+
scope_pattern: Scope pattern to match (e.g., 'lib:*')
31+
role: Role to match (e.g., 'role:library_admin')
32+
33+
Returns:
34+
int: Number of matching policies
35+
"""
36+
count = 0
37+
with open("openedx_authz/engine/config/authz.policy", "r") as f:
38+
for line in f:
39+
line = line.strip()
40+
if not line or line.startswith("#"):
41+
continue
42+
if not line.startswith("p,"):
43+
continue
44+
45+
parts = [p.strip() for p in line.split(",")]
46+
if len(parts) < 4:
47+
continue
48+
49+
# parts[0] = 'p', parts[1] = role, parts[2] = action, parts[3] = scope
50+
matches = True
51+
if role and parts[1] != role:
52+
matches = False
53+
if scope_pattern and parts[3] != scope_pattern:
54+
matches = False
55+
56+
if matches:
57+
count += 1
58+
return count
59+
2260
def _seed_database_with_policies(self):
2361
"""Seed the database with policies from the policy file.
2462
@@ -120,9 +158,16 @@ class TestPolicyLoadingStrategies(PolicyLoadingTestSetupMixin):
120158
121159
These tests demonstrate how policy loading would work in real-world scenarios,
122160
including scope-based loading, user-context loading, and role-specific loading.
123-
This provides examples for how the application should load policies in production.
161+
All based on our basic policy setup in authz.policy file.
124162
"""
125163

164+
LIBRARY_ROLES = [
165+
"role:library_user",
166+
"role:library_admin",
167+
"role:library_author",
168+
"role:library_collaborator",
169+
]
170+
126171
def setUp(self):
127172
"""Set up test environment without auto-loading policies."""
128173
super().setUp()
@@ -133,13 +178,12 @@ def tearDown(self):
133178
global_enforcer.clear_policy()
134179
super().tearDown()
135180

136-
@test_data(
137-
("lib:*", 4), # Library policies from authz.policy file
138-
("course:*", 0), # No course policies in basic setup
139-
("org:*", 0), # No org policies in basic setup
181+
@ddt_data(
182+
"lib:*", # Library policies from authz.policy file
183+
"course:*", # No course policies in basic setup
184+
"org:*", # No org policies in basic setup
140185
)
141-
@unpack
142-
def test_scope_based_policy_loading(self, scope, expected_policy_count):
186+
def test_scope_based_policy_loading(self, scope):
143187
"""Test loading policies for specific scopes.
144188
145189
This demonstrates how an application would load only policies
@@ -150,21 +194,26 @@ def test_scope_based_policy_loading(self, scope, expected_policy_count):
150194
- Only scope-relevant policies are loaded
151195
- Policy count matches expected for scope
152196
"""
197+
expected_policy_count = self._count_policies_in_file(scope_pattern=scope)
153198
initial_policy_count = len(global_enforcer.get_policy())
154199

155200
self._load_policies_for_scope(scope)
201+
loaded_policies = global_enforcer.get_policy()
156202

157203
self.assertEqual(initial_policy_count, 0)
158-
loaded_policies = global_enforcer.get_policy()
159204
self.assertEqual(len(loaded_policies), expected_policy_count)
160205

161-
# Verify that only policies for the requested scope are loaded
162206
if expected_policy_count > 0:
163207
scope_prefix = scope.replace("*", "")
164208
for policy in loaded_policies:
165209
self.assertTrue(policy[2].startswith(scope_prefix))
166210

167-
def test_user_context_policy_loading(self):
211+
@ddt_data(
212+
("user:alice", ["lib:*"]),
213+
("user:bob", ["lib:*"]),
214+
)
215+
@unpack
216+
def test_user_context_policy_loading(self, user, user_scopes):
168217
"""Test loading policies based on user context.
169218
170219
This demonstrates loading policies when a user logs in or
@@ -175,17 +224,16 @@ def test_user_context_policy_loading(self):
175224
- Policies are loaded for user's scopes
176225
- Policy count is reasonable for context
177226
"""
178-
user = "user:alice"
179-
user_scopes = ["lib:math_101", "lib:science_301"]
180227
initial_policy_count = len(global_enforcer.get_policy())
181228

182229
self._load_policies_for_user_context(user, user_scopes)
230+
loaded_policies = global_enforcer.get_policy()
183231

184232
self.assertEqual(initial_policy_count, 0)
185-
loaded_policies = global_enforcer.get_policy()
186233
self.assertGreater(len(loaded_policies), 0)
187234

188-
def test_role_specific_policy_loading(self):
235+
@ddt_data(*LIBRARY_ROLES)
236+
def test_role_specific_policy_loading(self, role_name):
189237
"""Test loading policies for specific role management operations.
190238
191239
This demonstrates loading policies when performing administrative
@@ -196,13 +244,12 @@ def test_role_specific_policy_loading(self):
196244
- Role-specific policies are loaded
197245
- Loaded policies contain expected role
198246
"""
199-
role_name = "role:library_admin"
200247
initial_policy_count = len(global_enforcer.get_policy())
201248

202249
self._load_policies_for_role_management(role_name)
250+
loaded_policies = global_enforcer.get_policy()
203251

204252
self.assertEqual(initial_policy_count, 0)
205-
loaded_policies = global_enforcer.get_policy()
206253
self.assertGreater(len(loaded_policies), 0)
207254

208255
role_found = any(role_name in str(policy) for policy in loaded_policies)
@@ -220,19 +267,23 @@ def test_policy_loading_lifecycle(self):
220267
- No policies exist at startup
221268
"""
222269
startup_policy_count = len(global_enforcer.get_policy())
270+
223271
self.assertEqual(startup_policy_count, 0)
224272

225273
self._load_policies_for_scope("lib:*")
226274
library_policy_count = len(global_enforcer.get_policy())
275+
227276
self.assertGreater(library_policy_count, 0)
228277

229278
self._load_policies_for_role_management("role:library_admin")
230279
admin_policy_count = len(global_enforcer.get_policy())
280+
231281
self.assertLessEqual(admin_policy_count, library_policy_count)
232282

233-
self._load_policies_for_user_context("user:alice", ["lib:math_101"])
283+
self._load_policies_for_user_context("user:alice", ["lib:*"])
234284
user_policy_count = len(global_enforcer.get_policy())
235-
self.assertGreaterEqual(user_policy_count, 0)
285+
286+
self.assertEqual(user_policy_count, library_policy_count)
236287

237288
def test_empty_enforcer_behavior(self):
238289
"""Test behavior when no policies are loaded.
@@ -246,15 +297,14 @@ def test_empty_enforcer_behavior(self):
246297
- No enforcement decisions are possible
247298
"""
248299
initial_policy_count = len(global_enforcer.get_policy())
249-
250300
all_policies = global_enforcer.get_policy()
251301
all_grouping_policies = global_enforcer.get_grouping_policy()
252302

253303
self.assertEqual(initial_policy_count, 0)
254304
self.assertEqual(len(all_policies), 0)
255305
self.assertEqual(len(all_grouping_policies), 0)
256306

257-
@test_data(
307+
@ddt_data(
258308
Filter(v2=["lib:*"]), # Load all library policies
259309
Filter(v2=["course:*"]), # Load all course policies
260310
Filter(v2=["org:*"]), # Load all organization policies
@@ -277,76 +327,87 @@ def test_filtered_policy_loading_variations(self, policy_filter):
277327

278328
global_enforcer.clear_policy()
279329
global_enforcer.load_filtered_policy(policy_filter)
330+
280331
loaded_policies = global_enforcer.get_policy()
281332

282333
self.assertEqual(initial_policy_count, 0)
283334
self.assertGreaterEqual(len(loaded_policies), 0)
284335

285-
def test_policy_reload_scenarios(self):
286-
"""Test policy reloading in different scenarios.
287-
288-
This demonstrates how policies can be reloaded when application
289-
context changes or when fresh policy data is needed.
336+
def test_policy_clear_and_reload(self):
337+
"""Test clearing and reloading policies maintains consistency.
290338
291339
Expected result:
292-
- Each reload operation works correctly
293-
- Policy counts change appropriately
294-
- No errors occur during transitions
340+
- Cleared enforcer has no policies
341+
- Reloading produces same count as initial load
295342
"""
296343
self._load_policies_for_scope("lib:*")
297-
first_load_count = len(global_enforcer.get_policy())
298-
self.assertGreater(first_load_count, 0)
344+
initial_load_count = len(global_enforcer.get_policy())
345+
346+
self.assertGreater(initial_load_count, 0)
299347

300348
global_enforcer.clear_policy()
301349
cleared_count = len(global_enforcer.get_policy())
350+
302351
self.assertEqual(cleared_count, 0)
303352

304353
self._load_policies_for_scope("lib:*")
305-
reload_count = len(global_enforcer.get_policy())
306-
self.assertEqual(reload_count, first_load_count)
354+
reloaded_count = len(global_enforcer.get_policy())
307355

308-
self._load_policies_for_role_management("role:library_user")
309-
filtered_count = len(global_enforcer.get_policy())
310-
self.assertLessEqual(filtered_count, first_load_count)
356+
self.assertEqual(reloaded_count, initial_load_count)
311357

312-
def test_multi_scope_filtering_demonstration(self):
313-
"""Test filtering across multiple scopes to demonstrate effectiveness.
358+
@ddt_data(*LIBRARY_ROLES)
359+
def test_filtered_loading_by_role(self, role_name):
360+
"""Test loading policies filtered by specific role.
314361
315-
This test shows that filtered loading actually works by comparing
316-
policy counts when loading different scope combinations.
362+
Expected result:
363+
- Filtered count matches policies in file for that role
364+
- All loaded policies contain the specified role
365+
"""
366+
expected_count = self._count_policies_in_file(role=role_name)
367+
368+
self._load_policies_for_role_management(role_name)
369+
loaded_policies = global_enforcer.get_policy()
370+
371+
self.assertEqual(len(loaded_policies), expected_count)
372+
373+
for policy in loaded_policies:
374+
self.assertIn(role_name, str(policy))
375+
376+
def test_multi_scope_filtering(self):
377+
"""Test filtering across multiple scopes.
317378
318379
Expected result:
319-
- Different scopes load different policy counts
320-
- Combined scopes load sum of individual scopes
321-
- Filtering is precise and predictable
380+
- Combined scope filter loads sum of individual scopes
381+
- Total load equals sum of all scope policies
322382
"""
323-
# Add test policies for multiple scopes
324-
self._add_test_policies_for_multiple_scopes()
383+
lib_scope = "lib:*"
384+
course_scope = "course:*"
385+
org_scope = "org:*"
325386

326-
# Load all policies to get baseline
327-
global_enforcer.load_policy()
328-
total_policy_count = len(global_enforcer.get_policy())
329-
self.assertGreater(total_policy_count, 0)
387+
expected_lib_count = self._count_policies_in_file(scope_pattern=lib_scope)
388+
self._add_test_policies_for_multiple_scopes()
330389

331-
# Test individual scope loading
332-
self._load_policies_for_scope("lib:*")
390+
self._load_policies_for_scope(lib_scope)
333391
lib_count = len(global_enforcer.get_policy())
334392

335-
self._load_policies_for_scope("course:*")
393+
self._load_policies_for_scope(course_scope)
336394
course_count = len(global_enforcer.get_policy())
337395

338-
self._load_policies_for_scope("org:*")
396+
self._load_policies_for_scope(org_scope)
339397
org_count = len(global_enforcer.get_policy())
340398

341-
# Test combined scope loading
399+
self.assertEqual(lib_count, expected_lib_count)
400+
self.assertEqual(course_count, 6)
401+
self.assertEqual(org_count, 3)
402+
342403
global_enforcer.clear_policy()
343-
multi_scope_filter = Filter(v2=["lib:*", "course:*"])
344-
global_enforcer.load_filtered_policy(multi_scope_filter)
404+
combined_filter = Filter(v2=[lib_scope, course_scope])
405+
global_enforcer.load_filtered_policy(combined_filter)
345406
combined_count = len(global_enforcer.get_policy())
346407

347-
# Verify filtering works as expected
348-
self.assertEqual(lib_count, 4)
349-
self.assertEqual(course_count, 6)
350-
self.assertEqual(org_count, 3)
351408
self.assertEqual(combined_count, lib_count + course_count)
352-
self.assertEqual(total_policy_count, lib_count + course_count + org_count)
409+
410+
global_enforcer.load_policy()
411+
total_count = len(global_enforcer.get_policy())
412+
413+
self.assertEqual(total_count, lib_count + course_count + org_count)

0 commit comments

Comments
 (0)