Skip to content

Commit d875701

Browse files
refactor: initialize enforcer when firstly use to avoid raising errors while initializing application
1 parent bcae860 commit d875701

8 files changed

Lines changed: 55 additions & 45 deletions

File tree

openedx_authz/api/permissions.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from openedx_authz.api.data import ActionData, PermissionData, PolicyIndex, ScopeData, SubjectData
99
from openedx_authz.engine.enforcer import AuthzEnforcer
1010

11-
enforcer = AuthzEnforcer.get_enforcer()
12-
1311
__all__ = [
1412
"get_permission_from_policy",
1513
"get_all_permissions_in_scope",
@@ -44,7 +42,7 @@ def get_all_permissions_in_scope(scope: ScopeData) -> list[PermissionData]:
4442
Returns:
4543
list of PermissionData: A list of PermissionData objects associated with the given scope.
4644
"""
47-
actions = enforcer.get_filtered_policy(
45+
actions = AuthzEnforcer.get_enforcer().get_filtered_policy(
4846
PolicyIndex.SCOPE.value, scope.namespaced_key
4947
)
5048
return [get_permission_from_policy(action) for action in actions]
@@ -65,6 +63,6 @@ def is_subject_allowed(
6563
Returns:
6664
bool: True if the subject has the specified permission in the scope, False otherwise.
6765
"""
68-
return enforcer.enforce(
66+
return AuthzEnforcer.get_enforcer().enforce(
6967
subject.namespaced_key, action.namespaced_key, scope.namespaced_key
7068
)

openedx_authz/api/roles.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from openedx_authz.api.permissions import get_permission_from_policy
2323
from openedx_authz.engine.enforcer import AuthzEnforcer
2424

25-
enforcer = AuthzEnforcer.get_enforcer()
2625

2726
__all__ = [
2827
"get_permissions_for_single_role",
@@ -61,7 +60,7 @@ def get_permissions_for_single_role(
6160
Returns:
6261
list[PermissionData]: A list of PermissionData objects associated with the given role.
6362
"""
64-
policies = enforcer.get_implicit_permissions_for_user(role.namespaced_key)
63+
policies = AuthzEnforcer.get_enforcer().get_implicit_permissions_for_user(role.namespaced_key)
6564
return [get_permission_from_policy(policy) for policy in policies]
6665

6766

@@ -116,7 +115,7 @@ def get_permissions_for_active_roles_in_scope(
116115
dict[str, list[PermissionData]]: A dictionary mapping the role external_key to its
117116
permissions and scopes.
118117
"""
119-
filtered_policy = enforcer.get_filtered_grouping_policy(
118+
filtered_policy = AuthzEnforcer.get_enforcer().get_filtered_grouping_policy(
120119
GroupingPolicyIndex.SCOPE.value, scope.namespaced_key
121120
)
122121

@@ -147,7 +146,7 @@ def get_role_definitions_in_scope(scope: ScopeData) -> list[RoleData]:
147146
Returns:
148147
list[Role]: A list of roles.
149148
"""
150-
policy_filtered = enforcer.get_filtered_policy(
149+
policy_filtered = AuthzEnforcer.get_enforcer().get_filtered_policy(
151150
PolicyIndex.SCOPE.value, scope.namespaced_key
152151
)
153152

@@ -180,7 +179,7 @@ def get_all_roles_names() -> list[str]:
180179
Returns:
181180
list[str]: A list of role names.
182181
"""
183-
return enforcer.get_all_subjects()
182+
return AuthzEnforcer.get_enforcer().get_all_subjects()
184183

185184

186185
def get_all_roles_in_scope(scope: ScopeData) -> list[list[str]]:
@@ -192,7 +191,7 @@ def get_all_roles_in_scope(scope: ScopeData) -> list[list[str]]:
192191
Returns:
193192
list[list[str]]: A list of policies in the specified scope.
194193
"""
195-
return enforcer.get_filtered_grouping_policy(
194+
return AuthzEnforcer.get_enforcer().get_filtered_grouping_policy(
196195
GroupingPolicyIndex.SCOPE.value, scope.namespaced_key
197196
)
198197

@@ -206,7 +205,7 @@ def assign_role_to_subject_in_scope(
206205
subject: The ID of the subject.
207206
role: The role to assign.
208207
"""
209-
enforcer.add_role_for_user_in_domain(
208+
AuthzEnforcer.get_enforcer().add_role_for_user_in_domain(
210209
subject.namespaced_key,
211210
role.namespaced_key,
212211
scope.namespaced_key,
@@ -236,7 +235,7 @@ def unassign_role_from_subject_in_scope(
236235
role: The role to unassign.
237236
scope: The scope from which to unassign the role.
238237
"""
239-
enforcer.delete_roles_for_user_in_domain(
238+
AuthzEnforcer.get_enforcer().delete_roles_for_user_in_domain(
240239
subject.namespaced_key, role.namespaced_key, scope.namespaced_key
241240
)
242241

@@ -265,7 +264,7 @@ def get_subject_role_assignments(subject: SubjectData) -> list[RoleAssignmentDat
265264
list[RoleAssignmentData]: A list of role assignments for the subject.
266265
"""
267266
role_assignments = []
268-
for policy in enforcer.get_filtered_grouping_policy(
267+
for policy in AuthzEnforcer.get_enforcer().get_filtered_grouping_policy(
269268
GroupingPolicyIndex.SUBJECT.value, subject.namespaced_key
270269
):
271270
role = RoleData(namespaced_key=policy[GroupingPolicyIndex.ROLE.value])
@@ -295,7 +294,7 @@ def get_subject_role_assignments_in_scope(
295294
"""
296295
# TODO: we still need to get the remaining data for the role like email, etc
297296
role_assignments = []
298-
for namespaced_key in enforcer.get_roles_for_user_in_domain(
297+
for namespaced_key in AuthzEnforcer.get_enforcer().get_roles_for_user_in_domain(
299298
subject.namespaced_key, scope.namespaced_key
300299
):
301300
role = RoleData(namespaced_key=namespaced_key)
@@ -327,7 +326,7 @@ def get_subject_role_assignments_for_role_in_scope(
327326
list[RoleAssignmentData]: A list of subjects assigned to the specified role in the specified scope.
328327
"""
329328
role_assignments = []
330-
for subject in enforcer.get_users_for_role_in_domain(
329+
for subject in AuthzEnforcer.get_enforcer().get_users_for_role_in_domain(
331330
role.namespaced_key, scope.namespaced_key
332331
):
333332
if subject.startswith(f"{RoleData.NAMESPACE}{RoleData.SEPARATOR}"):

openedx_authz/apps.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ class OpenedxAuthzConfig(AppConfig):
4343

4444
def ready(self):
4545
"""Initialization layer for the openedx_authz app."""
46-
# Initialize the enforcer to ensure it's ready when the app starts
47-
try:
48-
from openedx_authz.engine.enforcer import AuthzEnforcer
49-
AuthzEnforcer()
50-
except ImproperlyConfigured:
51-
# The app might not be fully configured yet (e.g., during migrations).
52-
# In such cases, we skip the enforcer initialization.
53-
pass
46+
# DO NOT initialize the enforcer here to avoid issues when
47+
# apps are not fully loaded (e.g., while pulling translations).
48+
# It's best to lazy load the enforcer when needed it's first used.
49+
pass

openedx_authz/engine/apps.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ class CasbinAdapterConfig(AppConfig):
88

99
def ready(self):
1010
"""Initialization layer for the casbin_adapter app."""
11-
12-
try:
13-
from casbin_adapter.enforcer import initialize_enforcer
14-
15-
db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default")
16-
initialize_enforcer(db_alias)
17-
except ImproperlyConfigured:
18-
# The app might not be fully configured yet (e.g., during migrations).
19-
# In such cases, we skip the enforcer initialization.
20-
pass
11+
# DO NOT initialize the enforcer here to avoid issues when
12+
# apps are not fully loaded (e.g., while pulling translations).
13+
# It's best to lazy load the enforcer when needed it's first used.
14+
pass

openedx_authz/engine/enforcer.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from openedx_authz.engine.adapter import ExtendedAdapter
2525
from openedx_authz.engine.watcher import Watcher
26+
from casbin_adapter.enforcer import initialize_enforcer
2627

2728
logger = logging.getLogger(__name__)
2829

@@ -33,9 +34,17 @@ class AuthzEnforcer:
3334
Ensures a single enforcer instance is created safely and configured with the
3435
ExtendedAdapter and Redis watcher for policy management and synchronization.
3536
36-
Usage:
37+
There are two main use cases for this class:
38+
1. Directly get the enforcer instance and initialize it if needed:
39+
from openedx_authz.engine.enforcer import AuthzEnforcer
3740
enforcer = AuthzEnforcer.get_enforcer()
3841
allowed = enforcer.enforce(user, resource, action)
42+
2. Instantiate the class to get the singleton enforcer instance:
43+
from openedx_authz.engine.enforcer import AuthzEnforcer
44+
enforcer = AuthzEnforcer()
45+
allowed = enforcer.get_enforcer().enforce(user, resource, action)
46+
47+
Any of the two approaches will yield the same singleton enforcer instance.
3948
"""
4049

4150
_enforcer = None
@@ -62,13 +71,16 @@ def _initialize_enforcer() -> FastEnforcer:
6271
"""
6372
Create and configure the Casbin FastEnforcer instance.
6473
65-
This function initializes the Casbin FastEnforcer with the ExtendedAdapter
66-
for database-backed policy storage and sets up the Redis watcher for
67-
real-time policy synchronization.
74+
This method initializes the FastEnforcer with the ExtendedAdapter
75+
for database policy storage and sets up the Redis watcher for real-time
76+
policy synchronization if the Watcher is available. It also initializes
77+
the enforcer with the specified database alias from settings.
6878
6979
Returns:
7080
FastEnforcer: Configured Casbin enforcer with adapter and watcher
7181
"""
82+
db_alias = getattr(settings, "CASBIN_DB_ALIAS", "default")
83+
initialize_enforcer(db_alias)
7284
adapter = ExtendedAdapter()
7385
enforcer = FastEnforcer(settings.CASBIN_MODEL, adapter, enable_log=True)
7486
enforcer.enable_auto_save(True)

openedx_authz/management/commands/load_policies.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from openedx_authz.engine.enforcer import AuthzEnforcer
1616
from openedx_authz.engine.utils import migrate_policy_between_enforcers
1717

18-
global_enforcer = AuthzEnforcer.get_enforcer()
1918

2019

2120
class Command(BaseCommand):
@@ -76,7 +75,7 @@ def handle(self, *args, **options):
7675
)
7776

7877
source_enforcer = casbin.Enforcer(model_file_path, policy_file_path)
79-
self.migrate_policies(source_enforcer, global_enforcer)
78+
self.migrate_policies(source_enforcer, AuthzEnforcer.get_enforcer())
8079

8180
def migrate_policies(self, source_enforcer, target_enforcer):
8281
"""Migrate policies from the source enforcer to the target enforcer.

openedx_authz/tests/api/test_roles.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from openedx_authz.engine.enforcer import AuthzEnforcer
3535
from openedx_authz.engine.utils import migrate_policy_between_enforcers
3636

37-
global_enforcer = AuthzEnforcer.get_enforcer()
3837

3938

4039
class RolesTestSetupMixin(TestCase):
@@ -47,6 +46,7 @@ def _seed_database_with_policies(cls):
4746
This simulates the one-time database seeding that would happen
4847
during application deployment, separate from the runtime policy loading.
4948
"""
49+
global_enforcer = AuthzEnforcer.get_enforcer()
5050
global_enforcer.load_policy()
5151
migrate_policy_between_enforcers(
5252
source_enforcer=casbin.Enforcer(
@@ -218,12 +218,12 @@ def setUpClass(cls):
218218
def setUp(self):
219219
"""Set up test environment."""
220220
super().setUp()
221-
global_enforcer.load_policy() # Load policies before each test to simulate fresh start
221+
AuthzEnforcer.get_enforcer().load_policy() # Load policies before each test to simulate fresh start
222222

223223
def tearDown(self):
224224
"""Clean up after each test to ensure isolation."""
225225
super().tearDown()
226-
global_enforcer.clear_policy() # Clear policies after each test to ensure isolation
226+
AuthzEnforcer.get_enforcer().clear_policy() # Clear policies after each test to ensure isolation
227227

228228

229229
@ddt

openedx_authz/tests/test_enforcer.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
from openedx_authz.engine.filter import Filter
1515
from openedx_authz.engine.utils import migrate_policy_between_enforcers
1616

17-
global_enforcer = AuthzEnforcer.get_enforcer()
18-
1917

2018
class PolicyLoadingTestSetupMixin(TestCase):
2119
"""Mixin providing policy loading test utilities."""
@@ -65,6 +63,7 @@ def _seed_database_with_policies(self):
6563
during application deployment, separate from runtime policy loading.
6664
"""
6765
# Always start with completely clean state
66+
global_enforcer = AuthzEnforcer.get_enforcer()
6867
global_enforcer.clear_policy()
6968

7069
migrate_policy_between_enforcers(
@@ -87,6 +86,7 @@ def _load_policies_for_scope(self, scope: str = None):
8786
scope: The scope to load policies for (e.g., 'lib^*' for all libraries).
8887
If None, loads all policies using load_policy().
8988
"""
89+
global_enforcer = AuthzEnforcer.get_enforcer()
9090
if scope is None:
9191
global_enforcer.load_policy()
9292
else:
@@ -99,6 +99,7 @@ def _load_policies_for_user_context(self, scopes: list[str] = None):
9999
Args:
100100
scopes: List of scopes the user is operating in.
101101
"""
102+
global_enforcer = AuthzEnforcer.get_enforcer()
102103
global_enforcer.clear_policy()
103104

104105
if scopes:
@@ -116,6 +117,7 @@ def _load_policies_for_role_management(self, role_name: str = None):
116117
Args:
117118
role_name: Specific role to load policies for, if any.
118119
"""
120+
global_enforcer = AuthzEnforcer.get_enforcer()
119121
global_enforcer.clear_policy()
120122

121123
if role_name:
@@ -131,6 +133,7 @@ def _add_test_policies_for_multiple_scopes(self):
131133
This adds course and organization policies in addition to existing
132134
library policies to create a realistic multi-scope environment.
133135
"""
136+
global_enforcer = AuthzEnforcer.get_enforcer()
134137
test_policies = [
135138
# Course policies
136139
["role^course_instructor", "act^edit_course", "course^*", "allow"],
@@ -172,7 +175,7 @@ def setUp(self):
172175

173176
def tearDown(self):
174177
"""Clean up after each test to ensure isolation."""
175-
global_enforcer.clear_policy()
178+
AuthzEnforcer.get_enforcer().clear_policy()
176179
super().tearDown()
177180

178181
@ddt_data(
@@ -191,6 +194,7 @@ def test_scope_based_policy_loading(self, scope):
191194
- Only scope-relevant policies are loaded
192195
- Policy count matches expected for scope
193196
"""
197+
global_enforcer = AuthzEnforcer.get_enforcer()
194198
expected_policy_count = self._count_policies_in_file(scope_pattern=scope)
195199
initial_policy_count = len(global_enforcer.get_policy())
196200

@@ -221,6 +225,7 @@ def test_user_context_policy_loading(self, user_scopes):
221225
- Policies are loaded for user's scopes
222226
- Policy count is reasonable for context
223227
"""
228+
global_enforcer = AuthzEnforcer.get_enforcer()
224229
initial_policy_count = len(global_enforcer.get_policy())
225230

226231
self._load_policies_for_user_context(user_scopes)
@@ -241,6 +246,7 @@ def test_role_specific_policy_loading(self, role_name):
241246
- Role-specific policies are loaded
242247
- Loaded policies contain expected role
243248
"""
249+
global_enforcer = AuthzEnforcer.get_enforcer()
244250
initial_policy_count = len(global_enforcer.get_policy())
245251

246252
self._load_policies_for_role_management(role_name)
@@ -263,6 +269,7 @@ def test_policy_loading_lifecycle(self):
263269
- Policy counts change appropriately between stages
264270
- No policies exist at startup
265271
"""
272+
global_enforcer = AuthzEnforcer.get_enforcer()
266273
startup_policy_count = len(global_enforcer.get_policy())
267274

268275
self.assertEqual(startup_policy_count, 0)
@@ -293,6 +300,7 @@ def test_empty_enforcer_behavior(self):
293300
- Policy queries return empty results
294301
- No enforcement decisions are possible
295302
"""
303+
global_enforcer = AuthzEnforcer.get_enforcer()
296304
initial_policy_count = len(global_enforcer.get_policy())
297305
all_policies = global_enforcer.get_policy()
298306
all_grouping_policies = global_enforcer.get_grouping_policy()
@@ -320,6 +328,7 @@ def test_filtered_policy_loading_variations(self, policy_filter):
320328
- Filtered loading works without errors
321329
- Appropriate policies are loaded based on filter
322330
"""
331+
global_enforcer = AuthzEnforcer.get_enforcer()
323332
initial_policy_count = len(global_enforcer.get_policy())
324333

325334
global_enforcer.clear_policy()
@@ -337,6 +346,7 @@ def test_policy_clear_and_reload(self):
337346
- Cleared enforcer has no policies
338347
- Reloading produces same count as initial load
339348
"""
349+
global_enforcer = AuthzEnforcer.get_enforcer()
340350
self._load_policies_for_scope("lib^*")
341351
initial_load_count = len(global_enforcer.get_policy())
342352

@@ -360,6 +370,7 @@ def test_filtered_loading_by_role(self, role_name):
360370
- Filtered count matches policies in file for that role
361371
- All loaded policies contain the specified role
362372
"""
373+
global_enforcer = AuthzEnforcer.get_enforcer()
363374
expected_count = self._count_policies_in_file(role=role_name)
364375

365376
self._load_policies_for_role_management(role_name)
@@ -377,6 +388,7 @@ def test_multi_scope_filtering(self):
377388
- Combined scope filter loads sum of individual scopes
378389
- Total load equals sum of all scope policies
379390
"""
391+
global_enforcer = AuthzEnforcer.get_enforcer()
380392
lib_scope = "lib^*"
381393
course_scope = "course^*"
382394
org_scope = "org^*"

0 commit comments

Comments
 (0)