Open johnthagen opened 3 years ago
I was able to get something manually patched together, in case this is useful for others:
from typing import Any, List, Optional, Tuple, Type
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.handlers.wsgi import WSGIRequest
from django_downloadview import DownloadResponse, ObjectDownloadView
from rest_framework.authentication import BaseAuthentication, BasicAuthentication
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework_simplejwt.authentication import JWTAuthentication
class DRFAuthenticatedObjectDownloadView(ObjectDownloadView):
"""A generic file download view that automatically authenticates the user and
validates permissions using DRF middleware."""
permissions_class: Type[BasePermission] = DjangoObjectPermissions
# Note: This needs to be kept in sync with
# settings.py REST_FRAMEWORK DEFAULT_AUTHENTICATION_CLASSES
auth_classes: List[Type[BaseAuthentication]] = [BasicAuthentication, JWTAuthentication]
def authenticate(self, request: WSGIRequest) -> None:
"""Updates request.user if the client has sent headers that configured ``auth_classes``
successfully authenticate.
"""
for auth_class in self.auth_classes:
auth_resp: Optional[Tuple[User, None]] = auth_class().authenticate(request)
if auth_resp is not None:
request.user = auth_resp[0]
return
def has_permission(self, request: WSGIRequest) -> None:
"""Validate that the current User has appropriate access permissions to a Model.
Raises:
PermissionDenied: If the user does not have the required permissions.
"""
instance = self.get_object()
permissions = self.permissions_class()
if not (
permissions.has_permission(request, self)
and permissions.has_object_permission(request, self, instance)
):
raise PermissionDenied()
def get(self, request: WSGIRequest, *args: Any, **kwargs: Any) -> DownloadResponse:
"""Authenticate user and check permissions before returning the file download."""
self.authenticate(request)
self.has_permission(request)
return super().get(request, *args, **kwargs)
I guess this proxy would be useful as part of the code of django-downloadview what do you think?
Yes, some kind of integration story with DRF would be nice as DRF does not have a native story for how serve authenticated or accelerated-download files.
I've only implemented this for a project on ObjectDownloadView
, but we'd probably want this to be a mixin and usable by all of the django-downloadview
View
s?
I'm not sure on the exact best design for a general solution.
@johnthagen I'm trying to do something similar for StorageDownloadView.
Maybe could you help me with the implementation? At first, it would be enough if only the authentication is checked for a user.
I have modified your code, but I was unable to get it working correctly. It downloads the file even when the user is not logged in. It seems for me, that my get() function never runs...
See: https://github.com/encode/django-rest-framework/issues/7702
DRF does not currently have a way to authenticate file downloads or serve them efficiently through a reverse proxy such as NGINX.
Integrating
django-downloadview
with DRF is challenging becausedjango-downloadview
does not interact with DRF authentication middleware (e.g.REST_FRAMEWORK
:'DEFAULT_AUTHENTICATION_CLASSES'
.django-downloadview
views cannot take advantage of'rest_framework.authentication.BasicAuthentication'
or (third-party)'rest_framework_simplejwt.authentication.JWTAuthentication'
authentication middleware. This results in thedjango-downloadview
View
being presented theAnonymousUser
user, and thus being rejected.