Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
2 changes: 1 addition & 1 deletion edx_rest_framework_extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
""" edx Django REST Framework extensions. """

__version__ = '10.6.0' # pragma: no cover
__version__ = '10.6.1' # pragma: no cover
50 changes: 27 additions & 23 deletions edx_rest_framework_extensions/auth/jwt/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,34 @@
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
are set on all endpoints that use JWTAuthentication.
"""
_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,
Expand All @@ -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."
Expand Down Expand Up @@ -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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Not really introduced by you, but above we have:

view_permissions = list(getattr(view_class, 'permission_classes', []))

Is there a reason for the inconsistency?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like an older version of this code used to iterate over the permission_classes using list.pop():

https://github.com/pwnage101/edx-drf-extensions/blob/1d470a29a16bcaf5c9247c8236def8a4be56b077/edx_rest_framework_extensions/auth/jwt/middleware.py#L56-L57

The recursive helper function _iter_included_base_classes appears to support any iterable, so it shouldn't matter whether a list or tuple is passed. That said, I'm happy to make it consistently a list.

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


Expand Down