11"""Decorators for AuthZ-based permissions enforcement."""
22import logging
33from functools import wraps
4+ from collections .abc import Callable
45
6+ from django .contrib .auth .models import AbstractUser
57from opaque_keys import InvalidKeyError
68from opaque_keys .edx .keys import CourseKey , UsageKey
9+ from openedx .core .djangoapps .authz .constants import LEGACY_PERMISSION_HANDLER_MAP , LegacyPermission
710from openedx_authz import api as authz_api
811from rest_framework import status
912
10- from common .djangoapps .student .auth import has_studio_write_access , has_studio_read_access
1113from openedx .core import toggles as core_toggles
1214from openedx .core .lib .api .view_utils import DeveloperErrorViewMixin
1315
1416log = 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.
0 commit comments