|
2 | 2 |
|
3 | 3 | from unittest.mock import MagicMock, patch |
4 | 4 |
|
| 5 | +import ddt |
5 | 6 | from django.test import TestCase |
6 | 7 |
|
7 | 8 | from openedx_authz.rest_api.v1.permissions import ( |
8 | 9 | BaseScopePermission, |
| 10 | + ContentLibraryPermission, |
9 | 11 | CoursePermission, |
10 | 12 | DynamicScopePermission, |
11 | 13 | ) |
@@ -114,6 +116,58 @@ def test_one_scope_fails_returns_false(self, _): |
114 | 116 | self.assertFalse(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
115 | 117 |
|
116 | 118 |
|
| 119 | +@ddt.ddt |
| 120 | +class TestDynamicScopePermissionBulkScopesMixed(TestCase): |
| 121 | + """Test DynamicScopePermission bulk-scopes behaviour when mixing specific and org-level scopes. |
| 122 | +
|
| 123 | + Parameterized over lib (lib:Org:A / lib:Org:*) and course-v1 (course-v1:Org1+C1+2024 / course-v1:Org1+*) |
| 124 | + namespaces to verify that the AND-logic holds regardless of whether a scope targets a specific |
| 125 | + resource or an entire org. |
| 126 | + """ |
| 127 | + |
| 128 | + def setUp(self): |
| 129 | + self.perm = DynamicScopePermission() |
| 130 | + |
| 131 | + def test_mixed_namespaces_raises_value_error(self): |
| 132 | + """Mixing lib and course-v1 scopes in the same bulk request raises ValueError.""" |
| 133 | + request = _make_request(data={"scopes": ["lib:Org:A", "course-v1:Org1+C1+2024"]}) |
| 134 | + with self.assertRaises(ValueError): |
| 135 | + self.perm.has_permission(request, _make_view(required_permissions=["p"])) |
| 136 | + |
| 137 | + @ddt.data( |
| 138 | + (["lib:Org:A", "lib:Org:*"], "get"), |
| 139 | + (["course-v1:Org1+C1+2024", "course-v1:Org1+*"], "get"), |
| 140 | + ) |
| 141 | + @ddt.unpack |
| 142 | + @patch("openedx_authz.api.is_user_allowed", return_value=True) |
| 143 | + def test_specific_and_org_scope_both_pass_returns_true(self, scopes, method, _): |
| 144 | + """When the user has permission on both the specific scope and the org-level scope, access is granted.""" |
| 145 | + request = _make_request(data={"scopes": scopes}, method=method.upper()) |
| 146 | + self.assertTrue(self.perm.has_permission(request, _make_view(method=method, required_permissions=["p"]))) |
| 147 | + |
| 148 | + @ddt.data( |
| 149 | + (["lib:Org:A", "lib:Org:*"], "get"), |
| 150 | + (["course-v1:Org1+C1+2024", "course-v1:Org1+*"], "get"), |
| 151 | + ) |
| 152 | + @ddt.unpack |
| 153 | + @patch("openedx_authz.api.is_user_allowed", side_effect=[True, False]) |
| 154 | + def test_specific_passes_org_fails_returns_false(self, scopes, method, _): |
| 155 | + """When the user has permission on the specific scope but not the org-level scope, access is denied.""" |
| 156 | + request = _make_request(data={"scopes": scopes}, method=method.upper()) |
| 157 | + self.assertFalse(self.perm.has_permission(request, _make_view(method=method, required_permissions=["p"]))) |
| 158 | + |
| 159 | + @ddt.data( |
| 160 | + (["lib:Org:A", "lib:Org:*"], "get"), |
| 161 | + (["course-v1:Org1+C1+2024", "course-v1:Org1+*"], "get"), |
| 162 | + ) |
| 163 | + @ddt.unpack |
| 164 | + @patch("openedx_authz.api.is_user_allowed", side_effect=[False, True]) |
| 165 | + def test_specific_fails_org_passes_returns_false(self, scopes, method, _): |
| 166 | + """When the user has permission on the org-level scope but not the specific scope, access is denied.""" |
| 167 | + request = _make_request(data={"scopes": scopes}, method=method.upper()) |
| 168 | + self.assertFalse(self.perm.has_permission(request, _make_view(method=method, required_permissions=["p"]))) |
| 169 | + |
| 170 | + |
117 | 171 | class TestCoursePermission(TestCase): |
118 | 172 | """Test CoursePermission class.""" |
119 | 173 |
|
@@ -141,3 +195,56 @@ def test_scope_with_permission_denied(self, _): |
141 | 195 | """When the user lacks the required permission on the course scope, access is denied.""" |
142 | 196 | request = _make_request(data={"scope": "course-v1:Org1+C1+2024"}, method="GET") |
143 | 197 | self.assertFalse(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
| 198 | + |
| 199 | + @patch("openedx_authz.api.is_user_allowed", return_value=True) |
| 200 | + def test_org_scope_allowed(self, _): |
| 201 | + """An org-level course scope ('course-v1:Org1+*') grants access when the user has the required permission.""" |
| 202 | + request = _make_request(data={"scope": "course-v1:Org1+*"}, method="GET") |
| 203 | + self.assertTrue(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
| 204 | + |
| 205 | + @patch("openedx_authz.api.is_user_allowed", return_value=False) |
| 206 | + def test_org_scope_denied(self, _): |
| 207 | + """An org-level course scope ('course-v1:Org1+*') denies access when the user lacks the required permission.""" |
| 208 | + request = _make_request(data={"scope": "course-v1:Org1+*"}, method="GET") |
| 209 | + self.assertFalse(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
| 210 | + |
| 211 | + |
| 212 | +class TestContentLibraryPermission(TestCase): |
| 213 | + """Test ContentLibraryPermission class.""" |
| 214 | + |
| 215 | + def setUp(self): |
| 216 | + self.perm = ContentLibraryPermission() |
| 217 | + |
| 218 | + def test_no_scope_returns_false(self): |
| 219 | + """A request without any scope value is always rejected — there is nothing to authorize against.""" |
| 220 | + self.assertFalse(self.perm.has_permission(_make_request(), _make_view(required_permissions=["p"]))) |
| 221 | + |
| 222 | + def test_scope_no_decorator_returns_true(self): |
| 223 | + """When a scope is present but the view method has no @authz_permissions decorator, |
| 224 | + the endpoint is considered open and access is granted.""" |
| 225 | + request = _make_request(data={"scope": "lib:Org1:A"}) |
| 226 | + self.assertTrue(self.perm.has_permission(request, _make_view(required_permissions=None))) |
| 227 | + |
| 228 | + @patch("openedx_authz.api.is_user_allowed", return_value=True) |
| 229 | + def test_scope_with_permission_allowed(self, _): |
| 230 | + """When the user has the required permission on the given library scope, access is granted.""" |
| 231 | + request = _make_request(data={"scope": "lib:Org1:A"}, method="GET") |
| 232 | + self.assertTrue(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
| 233 | + |
| 234 | + @patch("openedx_authz.api.is_user_allowed", return_value=False) |
| 235 | + def test_scope_with_permission_denied(self, _): |
| 236 | + """When the user lacks the required permission on the library scope, access is denied.""" |
| 237 | + request = _make_request(data={"scope": "lib:Org1:A"}, method="GET") |
| 238 | + self.assertFalse(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
| 239 | + |
| 240 | + @patch("openedx_authz.api.is_user_allowed", return_value=True) |
| 241 | + def test_org_scope_allowed(self, _): |
| 242 | + """An org-level lib scope ('lib:Org1:*') grants access when the user has the required permission.""" |
| 243 | + request = _make_request(data={"scope": "lib:Org1:*"}, method="GET") |
| 244 | + self.assertTrue(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
| 245 | + |
| 246 | + @patch("openedx_authz.api.is_user_allowed", return_value=False) |
| 247 | + def test_org_scope_denied(self, _): |
| 248 | + """An org-level lib scope ('lib:Org1:*') denies access when the user lacks the required permission.""" |
| 249 | + request = _make_request(data={"scope": "lib:Org1:*"}, method="GET") |
| 250 | + self.assertFalse(self.perm.has_permission(request, _make_view(method="get", required_permissions=["p"]))) |
0 commit comments