jazzband / django-downloadview

Serve files with Django.
https://django-downloadview.readthedocs.io
Other
364 stars 58 forks source link

DjangoRestFramework Integration #175

Open johnthagen opened 3 years ago

johnthagen commented 3 years ago

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 because django-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 the django-downloadview View being presented the AnonymousUser user, and thus being rejected.

johnthagen commented 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)
Natim commented 3 years ago

I guess this proxy would be useful as part of the code of django-downloadview what do you think?

johnthagen commented 3 years ago

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 Views?

I'm not sure on the exact best design for a general solution.

radokristof commented 2 years ago

@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...