diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 871e54bd..767f08ee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,10 @@ Change Log Unreleased ---------- +[10.6.1] - 2025-07-14 +--------------------- +* fix: Expand DRF permission composition support to JwtRedirectToLoginIfUnauthenticatedMiddleware. + [10.6.0] - 2025-04-04 --------------------- * Added Support for ``django 5.2``. diff --git a/edx_rest_framework_extensions/__init__.py b/edx_rest_framework_extensions/__init__.py index 8ae62253..f5a593fe 100644 --- a/edx_rest_framework_extensions/__init__.py +++ b/edx_rest_framework_extensions/__init__.py @@ -1,3 +1,3 @@ """ edx Django REST Framework extensions. """ -__version__ = '10.6.0' # pragma: no cover +__version__ = '10.6.1' # pragma: no cover diff --git a/edx_rest_framework_extensions/auth/jwt/middleware.py b/edx_rest_framework_extensions/auth/jwt/middleware.py index ba57023b..04462eda 100644 --- a/edx_rest_framework_extensions/auth/jwt/middleware.py +++ b/edx_rest_framework_extensions/auth/jwt/middleware.py @@ -35,6 +35,27 @@ log = logging.getLogger(__name__) +def _iter_included_base_classes(view_permissions): + """ + Yield all the permissions that are encapsulated in provided view_permissions, directly or as + a part of DRF's composed permissions. + """ + # Not all permissions are classes, some will be OperandHolder + # objects from DRF. So we have to crawl all those and expand them to see + # if our target classes are inside the conditionals somewhere. + for permission in view_permissions: + # Composition using DRF native support in 3.9+: + # IsStaff | IsSuperuser -> [IsStaff, IsSuperuser] + # IsOwner | IsStaff | IsSuperuser -> [IsOwner | IsStaff, IsSuperuser] + if isinstance(permission, OperandHolder): + decomposed_permissions = [permission.op1_class, permission.op2_class] + yield from _iter_included_base_classes(decomposed_permissions) + elif isinstance(permission, SingleOperandHolder): + yield permission.op1_class + else: + yield permission + + class EnsureJWTAuthSettingsMiddleware(MiddlewareMixin): """ Django middleware object that ensures the proper Permission classes @@ -42,26 +63,6 @@ class EnsureJWTAuthSettingsMiddleware(MiddlewareMixin): """ _required_permission_classes = (NotJwtRestrictedApplication,) - def _iter_included_base_classes(self, view_permissions): - """ - Yield all the permissions that are encapsulated in provided view_permissions, directly or as - a part of DRF's composed permissions. - """ - # Not all permissions are classes, some will be OperandHolder - # objects from DRF. So we have to crawl all those and expand them to see - # if our target classes are inside the conditionals somewhere. - for permission in view_permissions: - # Composition using DRF native support in 3.9+: - # IsStaff | IsSuperuser -> [IsStaff, IsSuperuser] - # IsOwner | IsStaff | IsSuperuser -> [IsOwner | IsStaff, IsSuperuser] - if isinstance(permission, OperandHolder): - decomposed_permissions = [permission.op1_class, permission.op2_class] - yield from self._iter_included_base_classes(decomposed_permissions) - elif isinstance(permission, SingleOperandHolder): - yield permission.op1_class - else: - yield permission - def _add_missing_jwt_permission_classes(self, view_class): """ Adds permissions classes that should exist for Jwt based authentication, @@ -71,7 +72,7 @@ def _add_missing_jwt_permission_classes(self, view_class): view_permissions = list(getattr(view_class, 'permission_classes', [])) for perm_class in self._required_permission_classes: - if not _includes_base_class(self._iter_included_base_classes(view_permissions), perm_class): + if not _includes_base_class(_iter_included_base_classes(view_permissions), perm_class): message = ( "The view %s allows Jwt Authentication. The required permission class, %s,", " was automatically added." @@ -161,8 +162,11 @@ def _check_and_cache_login_required_found(self, view_func): Checks for LoginRedirectIfUnauthenticated permission and caches the result. """ view_class = _get_view_class(view_func) - view_permission_classes = getattr(view_class, 'permission_classes', tuple()) - is_login_required_found = _includes_base_class(view_permission_classes, LoginRedirectIfUnauthenticated) + view_permissions = getattr(view_class, 'permission_classes', tuple()) + is_login_required_found = _includes_base_class( + _iter_included_base_classes(view_permissions), + LoginRedirectIfUnauthenticated, + ) self._get_request_cache()[self._LOGIN_REQUIRED_FOUND_CACHE_KEY] = is_login_required_found