Skip to content

Commit 528ca50

Browse files
committed
feat: add validation of list of scopes must have homogeneous namespaces
1 parent c8d3d1c commit 528ca50

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

openedx_authz/rest_api/v1/permissions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class BaseScopePermission(BasePermission, metaclass=PermissionMeta):
5959
def get_scope_value(self, request) -> str | None:
6060
"""Extract the scope value from the request.
6161
62+
When a ``scopes`` list is provided, returns only the first element.
63+
This is intentional: bulk requests are expected to be homogeneous
64+
(all scopes must share the same namespace). Actual per-scope permission
65+
validation for bulk requests is handled in ``DynamicScopePermission``.
66+
6267
Args:
6368
request: The Django REST framework request object.
6469
@@ -92,6 +97,12 @@ def get_scope_namespace(self, request) -> str:
9297
>>> permission.get_scope_namespace(request)
9398
'global'
9499
"""
100+
scopes_list = request.data.get("scopes")
101+
if scopes_list and isinstance(scopes_list, list):
102+
if not self._scopes_have_homogeneous_namespaces(scopes_list):
103+
raise ValueError(
104+
f"Mixed scope namespaces in bulk request are not allowed: {scopes_list}"
105+
)
95106
scope_value = self.get_scope_value(request)
96107
if not scope_value:
97108
return self.NAMESPACE
@@ -100,6 +111,22 @@ def get_scope_namespace(self, request) -> str:
100111
except ValueError:
101112
return self.NAMESPACE
102113

114+
def _scopes_have_homogeneous_namespaces(self, scopes_list: list[str]) -> bool:
115+
"""Check that all scopes in the list share the same namespace.
116+
117+
Args:
118+
scopes_list: List of scope values to check.
119+
Returns:
120+
bool: True if all scopes share the same namespace, False otherwise.
121+
"""
122+
namespaces = set()
123+
for scope in scopes_list:
124+
try:
125+
namespaces.add(api.ScopeData(external_key=scope).NAMESPACE)
126+
except ValueError:
127+
pass
128+
return len(namespaces) <= 1
129+
103130
def has_permission(self, request, view) -> bool:
104131
"""Fallback permission check (deny by default).
105132
@@ -146,6 +173,9 @@ class DynamicScopePermission(BaseScopePermission):
146173
147174
Note:
148175
Superusers and staff members always have permission regardless of scope.
176+
Bulk requests (``scopes`` list) must be homogeneous — all scopes must share
177+
the same namespace (e.g., all ``course-v1:`` or all ``lib:``). Mixed namespaces
178+
will raise a ``ValueError`` during namespace resolution.
149179
"""
150180

151181
NAMESPACE: ClassVar[None] = None

openedx_authz/tests/rest_api/test_permissions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ def test_scope_is_string_returns_value(self):
6161
self.assertEqual(self.perm.get_scope_value(request), "lib:Org:A")
6262

6363

64+
class TestGetScopeNamespaceMixedScopes(TestCase):
65+
"""Test that get_scope_namespace enforces namespace homogeneity for bulk scopes."""
66+
67+
def setUp(self):
68+
self.perm = BaseScopePermission()
69+
70+
def test_mixed_namespaces_raises_value_error(self):
71+
"""Passing scopes from different namespaces in a single bulk request raises ValueError."""
72+
request = _make_request(data={"scopes": ["lib:Org:A", "course-v1:Org1+C1+2024"]})
73+
with self.assertRaises(ValueError):
74+
self.perm.get_scope_namespace(request)
75+
76+
def test_homogeneous_namespaces_does_not_raise(self):
77+
"""Passing scopes that all share the same namespace does not raise."""
78+
request = _make_request(data={"scopes": ["lib:Org:A", "lib:Org:B"]})
79+
# Should not raise — just verify it completes without error
80+
namespace = self.perm.get_scope_namespace(request)
81+
self.assertEqual(namespace, "lib")
82+
83+
6484
class TestDynamicScopePermissionBulkScopes(TestCase):
6585
"""Test bulk-scopes path in DynamicScopePermission.has_permission."""
6686

0 commit comments

Comments
 (0)