Skip to content

Commit 582c8bc

Browse files
committed
fixup! feat: introduce authz_permission_required decorator
1 parent 4fe15cf commit 582c8bc

File tree

6 files changed

+96
-42
lines changed

6 files changed

+96
-42
lines changed

cms/djangoapps/contentstore/api/tests/test_quality.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,29 @@ def test_student_fails(self):
7979
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
8080

8181

82-
@patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, "is_enabled", return_value=True)
8382
class CourseQualityAuthzTest(BaseCourseViewTest):
8483
"""
8584
Tests Course Quality API authorization using openedx-authz.
8685
"""
8786

8887
view_name = "courses_api:course_quality"
8988

89+
@classmethod
90+
def setUpClass(cls):
91+
cls.toggle_patcher = patch.object(
92+
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
93+
"is_enabled",
94+
return_value=True
95+
)
96+
cls.toggle_patcher.start()
97+
98+
super().setUpClass()
99+
100+
@classmethod
101+
def tearDownClass(cls):
102+
cls.toggle_patcher.stop()
103+
super().tearDownClass()
104+
90105
def setUp(self):
91106
super().setUp()
92107

@@ -137,24 +152,24 @@ def _seed_database_with_policies(cls):
137152
target_enforcer=global_enforcer,
138153
)
139154

140-
def test_authorized_user_can_access(self, mock_flag):
155+
def test_authorized_user_can_access(self):
141156
"""User with COURSE_STAFF role can access."""
142157
resp = self.authorized_client.get(self.get_url(self.course_key))
143158
self.assertEqual(resp.status_code, status.HTTP_200_OK)
144159

145-
def test_unauthorized_user_cannot_access(self, mock_flag):
160+
def test_unauthorized_user_cannot_access(self):
146161
"""User without role cannot access."""
147162
resp = self.unauthorized_client.get(self.get_url(self.course_key))
148163
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
149164

150-
def test_role_scoped_to_course(self, mock_flag):
165+
def test_role_scoped_to_course(self):
151166
"""Authorization should only apply to the assigned course."""
152167
other_course = self.store.create_course("OtherOrg", "OtherCourse", "Run", self.staff.id)
153168

154169
resp = self.authorized_client.get(self.get_url(other_course.id))
155170
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
156171

157-
def test_staff_user_allowed_via_legacy(self, mock_flag):
172+
def test_staff_user_allowed_via_legacy(self):
158173
"""
159174
Staff users should still pass through legacy fallback.
160175
"""
@@ -163,7 +178,7 @@ def test_staff_user_allowed_via_legacy(self, mock_flag):
163178
resp = self.client.get(self.get_url(self.course_key))
164179
self.assertEqual(resp.status_code, status.HTTP_200_OK)
165180

166-
def test_superuser_allowed(self, mock_flag):
181+
def test_superuser_allowed(self):
167182
"""Superusers should always be allowed."""
168183
superuser = UserFactory(is_superuser=True)
169184

cms/djangoapps/contentstore/api/tests/test_validation.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,29 @@ def test_list_ready_to_update_reference_success(self, mock_block, mock_auth):
284284
mock_auth.assert_called_once()
285285

286286

287-
@patch.object(core_toggles.AUTHZ_COURSE_AUTHORING_FLAG, "is_enabled", return_value=True)
288287
class CourseValidationAuthzTest(BaseCourseViewTest):
289288
"""
290289
Tests Course Validation API authorization using openedx-authz.
291290
"""
292291

293292
view_name = "courses_api:course_validation"
294293

294+
@classmethod
295+
def setUpClass(cls):
296+
cls.toggle_patcher = patch.object(
297+
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
298+
"is_enabled",
299+
return_value=True
300+
)
301+
cls.toggle_patcher.start()
302+
303+
super().setUpClass()
304+
305+
@classmethod
306+
def tearDownClass(cls):
307+
cls.toggle_patcher.stop()
308+
super().tearDownClass()
309+
295310
def setUp(self):
296311
super().setUp()
297312

@@ -342,23 +357,23 @@ def _seed_database_with_policies(cls):
342357
target_enforcer=global_enforcer,
343358
)
344359

345-
def test_authorized_user_can_access(self, mock_flag):
360+
def test_authorized_user_can_access(self):
346361
"""
347362
User with COURSE_STAFF role should be allowed via AuthZ.
348363
"""
349364
resp = self.authorized_client.get(self.get_url(self.course_key))
350365

351366
self.assertEqual(resp.status_code, status.HTTP_200_OK)
352367

353-
def test_unauthorized_user_cannot_access(self, mock_flag):
368+
def test_unauthorized_user_cannot_access(self):
354369
"""
355370
User without permissions should be denied.
356371
"""
357372
resp = self.unauthorized_client.get(self.get_url(self.course_key))
358373

359374
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
360375

361-
def test_role_scoped_to_course(self, mock_flag):
376+
def test_role_scoped_to_course(self):
362377
"""
363378
Authorization should only apply to the assigned course scope.
364379
"""
@@ -373,7 +388,7 @@ def test_role_scoped_to_course(self, mock_flag):
373388

374389
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
375390

376-
def test_staff_user_allowed_via_legacy(self, mock_flag):
391+
def test_staff_user_allowed_via_legacy(self):
377392
"""
378393
Course staff should pass through legacy fallback when AuthZ denies.
379394
"""
@@ -383,7 +398,7 @@ def test_staff_user_allowed_via_legacy(self, mock_flag):
383398

384399
self.assertEqual(resp.status_code, status.HTTP_200_OK)
385400

386-
def test_superuser_allowed(self, mock_flag):
401+
def test_superuser_allowed(self):
387402
"""
388403
Superusers should always be allowed through legacy fallback.
389404
"""

openedx/core/djangoapps/authz/decorators.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77
from openedx_authz import api as authz_api
88
from rest_framework import status
99

10-
from common.djangoapps.student.auth import has_course_author_access
10+
from common.djangoapps.student.auth import has_studio_write_access, has_studio_read_access
1111
from openedx.core import toggles as core_toggles
1212
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
1313

1414
log = logging.getLogger(__name__)
1515

1616

17+
legacy_permission_handler_map = {
18+
"read": has_studio_read_access,
19+
"write": has_studio_write_access,
20+
}
21+
22+
1723
def authz_permission_required(authz_permission, legacy_permission=None):
1824
"""
1925
Decorator enforcing course author permissions via AuthZ
@@ -49,20 +55,23 @@ def user_has_course_permission(user, authz_permission, course_key, legacy_permis
4955
"""
5056
Core authorization logic.
5157
"""
58+
if core_toggles.enable_authz_course_authoring(course_key):
59+
# If AuthZ is enabled for this course, check the permission via AuthZ only.
60+
is_user_allowed = authz_api.is_user_allowed(user.username, authz_permission, str(course_key))
61+
log.info(
62+
"AuthZ permission granted = {}".format(is_user_allowed),
63+
extra={
64+
"user_id": user.id,
65+
"authz_permission": authz_permission,
66+
"course_key": str(course_key),
67+
},
68+
)
69+
return is_user_allowed
5270

53-
if is_authz_enabled(course_key):
54-
if authz_api.is_user_allowed(user.username, authz_permission, str(course_key)):
55-
log.info(
56-
"AuthZ permission granted",
57-
extra={
58-
"user_id": user.id,
59-
"authz_permission": authz_permission,
60-
"course_key": str(course_key),
61-
},
62-
)
63-
return True
64-
65-
if legacy_permission and has_course_author_access(user, course_key):
71+
# If AuthZ is not enabled for this course, fall back to legacy course author
72+
# access check if legacy_permission is provided.
73+
has_legacy_permission = legacy_permission_handler_map.get(legacy_permission)
74+
if legacy_permission and has_legacy_permission and has_legacy_permission(user, course_key):
6675
log.info(
6776
"AuthZ fallback used",
6877
extra={
@@ -85,11 +94,6 @@ def user_has_course_permission(user, authz_permission, course_key, legacy_permis
8594
return False
8695

8796

88-
def is_authz_enabled(course_key):
89-
"""Check if AuthZ is enabled for this course."""
90-
return core_toggles.AUTHZ_COURSE_AUTHORING_FLAG.is_enabled(course_key)
91-
92-
9397
def get_course_key(course_id):
9498
"""
9599
Given a course_id string, attempts to parse it as a CourseKey.

openedx/core/djangoapps/authz/models.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

openedx/core/djangoapps/authz/tests/test_decorators.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,20 @@ def test_view_executes_when_permission_granted(self):
4949
self.course_key,
5050
)
5151

52-
def test_view_executes_when_legacy_fallback(self):
52+
def test_view_executes_when_legacy_fallback_read(self):
5353
"""Decorator allows execution when AuthZ denies but legacy permission succeeds."""
5454
request = self._build_request()
5555

5656
mock_view = Mock(return_value="success")
5757

5858
with patch(
59-
"openedx.core.djangoapps.authz.decorators.is_authz_enabled",
60-
return_value=True,
59+
"openedx.core.djangoapps.authz.decorators.core_toggles.enable_authz_course_authoring",
60+
return_value=False,
6161
), patch(
6262
"openedx.core.djangoapps.authz.decorators.authz_api.is_user_allowed",
63-
return_value=False,
63+
return_value=True, # Should not be used when AuthZ is disabled, but set to True just in case
6464
), patch(
65-
"openedx.core.djangoapps.authz.decorators.has_course_author_access",
65+
"openedx.core.djangoapps.authz.decorators.has_studio_read_access",
6666
return_value=True,
6767
):
6868
decorated = authz_permission_required(
@@ -75,6 +75,32 @@ def test_view_executes_when_legacy_fallback(self):
7575
self.assertEqual(result, "success")
7676
mock_view.assert_called_once()
7777

78+
def test_view_executes_when_legacy_fallback_write(self):
79+
"""Decorator allows execution when AuthZ denies but legacy write permission succeeds."""
80+
request = self._build_request()
81+
82+
mock_view = Mock(return_value="success")
83+
84+
with patch(
85+
"openedx.core.djangoapps.authz.decorators.core_toggles.enable_authz_course_authoring",
86+
return_value=False,
87+
), patch(
88+
"openedx.core.djangoapps.authz.decorators.authz_api.is_user_allowed",
89+
return_value=True, # Should not be used when AuthZ is disabled, but set to True just in case
90+
), patch(
91+
"openedx.core.djangoapps.authz.decorators.has_studio_write_access",
92+
return_value=True,
93+
):
94+
decorated = authz_permission_required(
95+
"courses.edit",
96+
legacy_permission="write"
97+
)(mock_view)
98+
99+
result = decorated(self.view_instance, request, str(self.course_key))
100+
101+
self.assertEqual(result, "success")
102+
mock_view.assert_called_once()
103+
78104
def test_access_denied_when_permission_fails(self):
79105
"""Decorator raises API error when permission fails."""
80106
request = self._build_request()

openedx/core/djangoapps/authz/views.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)