Skip to content

Commit 769e223

Browse files
refactor: add error management when getting scope subclasses
1 parent 92e1d3e commit 769e223

3 files changed

Lines changed: 41 additions & 16 deletions

File tree

openedx_authz/api/data.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
]
2222

2323
AUTHZ_POLICY_ATTRIBUTES_SEPARATOR = "^"
24+
EXTERNAL_KEY_SEPARATOR = ":"
2425

2526

2627
class GroupingPolicyIndex(Enum):
@@ -162,12 +163,21 @@ def get_subclass_by_external_key(mcs, external_key: str) -> Type["ScopeData"]:
162163
# even 'course-v1:edX+DemoX+2021_T1'. This won't work for org scopes because they don't explicitly indicate
163164
# the namespace in the external key. TODO: We need to handle org scopes differently.
164165
# 2. The namespace is always the part before the first separator.
165-
# 3. If the namespace is not recognized, we return the base ScopeData class
166-
# 4. The subclass implements a validation method to validate the entire key
167-
namespace = external_key.split(":", 1)[0]
166+
# 3. If the namespace is not recognized, we raise an error.
167+
# 4. The subclass implements a validation method to validate the entire key. E.g., ContentLibraryData
168+
# validates that the external_key is a valid library ID.
169+
if EXTERNAL_KEY_SEPARATOR not in external_key:
170+
raise ValueError(f"Invalid external_key format: {external_key}")
171+
172+
namespace = external_key.split(EXTERNAL_KEY_SEPARATOR, 1)[0]
168173
scope_subclass = mcs.scope_registry.get(namespace)
169-
if not scope_subclass or not scope_subclass.validate_external_key(external_key):
170-
return ScopeData # Fallback to base class if not found or invalid
174+
175+
if not scope_subclass:
176+
raise ValueError(f"Unknown scope: {namespace} for external_key: {external_key}")
177+
178+
if not scope_subclass.validate_external_key(external_key):
179+
raise ValueError(f"Invalid external_key format: {external_key}")
180+
171181
return scope_subclass
172182

173183
@classmethod
@@ -195,6 +205,22 @@ class ScopeData(AuthZData, metaclass=ScopeMeta):
195205

196206
NAMESPACE: ClassVar[str] = "sc"
197207

208+
@classmethod
209+
def validate_external_key(cls, external_key: str) -> bool:
210+
"""Validate the external_key format for ScopeData.
211+
212+
For the base ScopeData class, we accept any external_key works. This
213+
is only implemented for the sake of completeness. Subclasses should
214+
implement their own validation logic.
215+
216+
Args:
217+
external_key: The external key to validate.
218+
219+
Returns:
220+
bool: True if valid, False otherwise.
221+
"""
222+
return True
223+
198224

199225
@define
200226
class ContentLibraryData(ScopeData):

openedx_authz/tests/api/test_data.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def test_get_subclass_by_namespaced_key(self, namespaced_key, expected_class):
238238
@data(
239239
("lib:DemoX:CSPROB", ContentLibraryData),
240240
("lib:edX:Demo", ContentLibraryData),
241-
("unknown:something", ScopeData),
241+
("sc:generic_scope", ScopeData),
242242
)
243243
@unpack
244244
def test_get_subclass_by_external_key(self, external_key, expected_class):
@@ -247,7 +247,6 @@ def test_get_subclass_by_external_key(self, external_key, expected_class):
247247
Expected Result:
248248
- 'lib:...' returns ContentLibraryData
249249
- 'sc:...' returns ScopeData
250-
- 'unknown:...' returns ScopeData (fallback)
251250
"""
252251
subclass = ScopeMeta.get_subclass_by_external_key(external_key)
253252
self.assertIs(subclass, expected_class)
@@ -287,8 +286,8 @@ def test_base_scope_data_with_external_key(self):
287286
- ScopeData(external_key='...') creates ScopeData instance
288287
- No dynamic subclass selection occurs
289288
"""
290-
scope = ScopeData(external_key="generic_scope")
289+
scope = ScopeData(external_key="sc:generic_scope")
291290
self.assertIsInstance(scope, ScopeData)
292-
self.assertEqual(scope.external_key, "generic_scope")
293-
expected_namespaced = f"{ScopeData.NAMESPACE}{ScopeData.SEPARATOR}generic_scope"
291+
self.assertEqual(scope.external_key, "sc:generic_scope")
292+
expected_namespaced = f"{ScopeData.NAMESPACE}{ScopeData.SEPARATOR}sc:generic_scope"
294293
self.assertEqual(scope.namespaced_key, expected_namespaced)

openedx_authz/tests/api/test_roles.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -786,9 +786,9 @@ def test_get_all_role_assignments_scopes(self, subject_name, expected_roles):
786786
("library_author", "lib:Org6:project_beta", 1),
787787
("library_collaborator", "lib:Org6:project_gamma", 1),
788788
("library_user", "lib:Org6:project_delta", 1),
789-
("non_existent_role", "any_library", 0),
790-
("library_admin", "non_existent_scope", 0),
791-
("non_existent_role", "non_existent_scope", 0),
789+
("non_existent_role", "sc:any_library", 0),
790+
("library_admin", "sc:non_existent_scope", 0),
791+
("non_existent_role", "sc:non_existent_scope", 0),
792792
)
793793
@unpack
794794
def test_get_role_assignments_in_scope(self, role_name, scope_name, expected_count):
@@ -817,7 +817,7 @@ class TestRoleAssignmentAPI(RolesTestSetupMixin):
817817
"""
818818

819819
@ddt_data(
820-
(["mary", "john"], "library_user", "batch_test", True),
820+
(["mary", "john"], "library_user", "sc:batch_test", True),
821821
(
822822
["paul", "diana", "lila"],
823823
"library_collaborator",
@@ -876,7 +876,7 @@ def test_batch_assign_role_to_subjects_in_scope(
876876
self.assertIn(role, role_names)
877877

878878
@ddt_data(
879-
(["mary", "john"], "library_user", "batch_test", True),
879+
(["mary", "john"], "library_user", "sc:batch_test", True),
880880
(
881881
["paul", "diana", "lila"],
882882
"library_collaborator",
@@ -1115,7 +1115,7 @@ def test_unassign_role_from_subject_in_scope(
11151115
)
11161116
],
11171117
),
1118-
("non_existent_scope", []),
1118+
("sc:non_existent_scope", []),
11191119
)
11201120
@unpack
11211121
def test_get_all_role_assignments_in_scope(self, scope_name, expected_assignments):

0 commit comments

Comments
 (0)