Skip to content

Commit 45d595c

Browse files
committed
fixup! feat: introduce authz_permission_required decorator
1 parent b6a8ec7 commit 45d595c

File tree

8 files changed

+59
-22
lines changed

8 files changed

+59
-22
lines changed

cms/djangoapps/contentstore/api/views/course_quality.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from edxval.api import get_course_videos_qset
77
from rest_framework.generics import GenericAPIView
88
from rest_framework.response import Response
9+
from openedx.core.djangoapps.authz.constants import LegacyPermission
910
from scipy import stats
1011
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
1112

@@ -84,7 +85,7 @@ class CourseQualityView(DeveloperErrorViewMixin, GenericAPIView):
8485
# does not specify a serializer class.
8586
swagger_schema = None
8687

87-
@authz_permission_required(COURSES_VIEW_COURSE.identifier, legacy_permission="read")
88+
@authz_permission_required(COURSES_VIEW_COURSE.identifier, LegacyPermission.READ)
8889
def get(self, request, course_key):
8990
"""
9091
Returns validation information for the given course.

cms/djangoapps/contentstore/api/views/course_validation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from rest_framework import serializers, status
1010
from rest_framework.generics import GenericAPIView
1111
from rest_framework.response import Response
12+
from openedx.core.djangoapps.authz.constants import LegacyPermission
1213
from user_tasks.models import UserTaskStatus
1314
from user_tasks.views import StatusViewSet
1415
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
@@ -82,7 +83,7 @@ class CourseValidationView(DeveloperErrorViewMixin, GenericAPIView):
8283
# does not specify a serializer class.
8384
swagger_schema = None
8485

85-
@authz_permission_required(COURSES_VIEW_COURSE.identifier, legacy_permission="read")
86+
@authz_permission_required(COURSES_VIEW_COURSE.identifier, LegacyPermission.READ)
8687
def get(self, request, course_key):
8788
"""
8889
Returns validation information for the given course.

cms/envs/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,9 @@ def make_lms_template_path(settings):
904904
# alternative swagger generator for CMS API
905905
'drf_spectacular',
906906

907+
# Authz
908+
'openedx.core.djangoapps.authz',
909+
907910
'openedx_events',
908911

909912
# Core models to represent courses

lms/envs/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2019,7 +2019,7 @@
20192019
'openedx.core.djangoapps.notifications',
20202020

20212021
# Authz
2022-
'openedx.core.djangoapps.authz.apps.AuthzConfig',
2022+
'openedx.core.djangoapps.authz',
20232023

20242024
'openedx_events',
20252025

openedx/core/djangoapps/authz/apps.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55

66
class AuthzConfig(AppConfig):
7+
"""Django application configuration for the Open edX Authorization (AuthZ) app.
8+
9+
This app provides a centralized location for integrations with the
10+
openedx-authz library, including permission helpers, decorators,
11+
and other utilities used to enforce RBAC-based authorization across
12+
the platform."""
13+
714
default_auto_field = 'django.db.models.BigAutoField'
815
name = 'openedx.core.djangoapps.authz'
9-
verbose_name = "Authz"
16+
verbose_name = "Open edX Authorization Framework"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Constants used by the Open edX Authorization (AuthZ) framework."""
2+
3+
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
4+
from openedx.core.lib.teams_config import Enum
5+
6+
7+
class LegacyPermission(Enum):
8+
READ = "read"
9+
WRITE = "write"
10+
11+
12+
LEGACY_PERMISSION_HANDLER_MAP = {
13+
LegacyPermission.READ: has_studio_read_access,
14+
LegacyPermission.WRITE: has_studio_write_access,
15+
}

openedx/core/djangoapps/authz/decorators.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
11
"""Decorators for AuthZ-based permissions enforcement."""
22
import logging
33
from functools import wraps
4+
from collections.abc import Callable
45

6+
from django.contrib.auth.models import AbstractUser
57
from opaque_keys import InvalidKeyError
68
from opaque_keys.edx.keys import CourseKey, UsageKey
9+
from openedx.core.djangoapps.authz.constants import LEGACY_PERMISSION_HANDLER_MAP, LegacyPermission
710
from openedx_authz import api as authz_api
811
from rest_framework import status
912

10-
from common.djangoapps.student.auth import has_studio_write_access, has_studio_read_access
1113
from openedx.core import toggles as core_toggles
1214
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
1315

1416
log = logging.getLogger(__name__)
1517

1618

17-
legacy_permission_handler_map = {
18-
"read": has_studio_read_access,
19-
"write": has_studio_write_access,
20-
}
21-
22-
23-
def authz_permission_required(authz_permission, legacy_permission=None):
19+
def authz_permission_required(authz_permission: str, legacy_permission: LegacyPermission | None = None) -> Callable:
2420
"""
2521
Decorator enforcing course author permissions via AuthZ
2622
with optional legacy fallback.
23+
24+
This decorator checks if the requesting user has the specified AuthZ permission for the course.
25+
If AuthZ is not enabled for the course, and a legacy_permission is provided, it falls back to checking
26+
the legacy permission.
27+
28+
Raises:
29+
DeveloperErrorResponseException: If the user does not have the required permissions.
2730
"""
2831

2932
def decorator(view_func):
@@ -40,8 +43,8 @@ def _wrapped_view(self, request, course_id, *args, **kwargs):
4043
):
4144
raise DeveloperErrorViewMixin.api_error(
4245
status_code=status.HTTP_403_FORBIDDEN,
43-
developer_message="The requesting user does not have course author permissions.",
44-
error_code="user_permissions",
46+
developer_message="You do not have permission to perform this action.",
47+
error_code="permission_denied",
4548
)
4649

4750
return view_func(self, request, course_key, *args, **kwargs)
@@ -51,9 +54,15 @@ def _wrapped_view(self, request, course_id, *args, **kwargs):
5154
return decorator
5255

5356

54-
def user_has_course_permission(user, authz_permission, course_key, legacy_permission=None):
57+
def user_has_course_permission(
58+
user: AbstractUser,
59+
authz_permission: str,
60+
course_key: CourseKey,
61+
legacy_permission: LegacyPermission | None = None,
62+
) -> bool:
5563
"""
56-
Core authorization logic.
64+
Checks if the user has the specified AuthZ permission for the course,
65+
with optional fallback to legacy permissions.
5766
"""
5867
if core_toggles.enable_authz_course_authoring(course_key):
5968
# If AuthZ is enabled for this course, check the permission via AuthZ only.
@@ -70,7 +79,7 @@ def user_has_course_permission(user, authz_permission, course_key, legacy_permis
7079

7180
# If AuthZ is not enabled for this course, fall back to legacy course author
7281
# access check if legacy_permission is provided.
73-
has_legacy_permission = legacy_permission_handler_map.get(legacy_permission)
82+
has_legacy_permission: Callable | None = LEGACY_PERMISSION_HANDLER_MAP.get(legacy_permission)
7483
if legacy_permission and has_legacy_permission and has_legacy_permission(user, course_key):
7584
log.info(
7685
"AuthZ fallback used",
@@ -94,7 +103,7 @@ def user_has_course_permission(user, authz_permission, course_key, legacy_permis
94103
return False
95104

96105

97-
def get_course_key(course_id):
106+
def get_course_key(course_id: str) -> CourseKey:
98107
"""
99108
Given a course_id string, attempts to parse it as a CourseKey.
100109
If that fails, attempts to parse it as a UsageKey and extract the course key from it.

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.test import RequestFactory, TestCase
55
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
66

7+
from openedx.core.djangoapps.authz.constants import LegacyPermission
78
from openedx.core.djangoapps.authz.decorators import authz_permission_required, get_course_key
89
from openedx.core.lib.api.view_utils import DeveloperErrorResponseException
910

@@ -62,12 +63,12 @@ def test_view_executes_when_legacy_fallback_read(self):
6263
"openedx.core.djangoapps.authz.decorators.authz_api.is_user_allowed",
6364
return_value=True, # Should not be used when AuthZ is disabled, but set to True just in case
6465
), patch(
65-
"openedx.core.djangoapps.authz.decorators.has_studio_read_access",
66+
"openedx.core.djangoapps.authz.constants.has_studio_read_access",
6667
return_value=True,
6768
):
6869
decorated = authz_permission_required(
6970
"courses.view",
70-
legacy_permission="read"
71+
legacy_permission=LegacyPermission.READ
7172
)(mock_view)
7273

7374
result = decorated(self.view_instance, request, str(self.course_key))
@@ -88,12 +89,12 @@ def test_view_executes_when_legacy_fallback_write(self):
8889
"openedx.core.djangoapps.authz.decorators.authz_api.is_user_allowed",
8990
return_value=True, # Should not be used when AuthZ is disabled, but set to True just in case
9091
), patch(
91-
"openedx.core.djangoapps.authz.decorators.has_studio_write_access",
92+
"openedx.core.djangoapps.authz.constants.has_studio_write_access",
9293
return_value=True,
9394
):
9495
decorated = authz_permission_required(
9596
"courses.edit",
96-
legacy_permission="write"
97+
legacy_permission=LegacyPermission.WRITE
9798
)(mock_view)
9899

99100
result = decorated(self.view_instance, request, str(self.course_key))

0 commit comments

Comments
 (0)