Django-rest-framework: Document how to authenticate and serve private media files

Created on 26 Jan 2021  Â·  9Comments  Â·  Source: encode/django-rest-framework

Checklist

  • [x] I have verified that that issue exists against the master branch of Django REST framework.
  • [x] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
  • [x] This is not a usage question. (Those should be directed to the discussion group instead.)
  • [x] This cannot be dealt with as a third party library. (We prefer new functionality to be in the form of third party libraries where possible.)
  • [ ] I have reduced the issue to the simplest possible case.
  • [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

Expected behavior

The docs explain how to use FileFields to serve media file URLs, but if these files are sensitive and need authentication/permissions, there is no mention of how to authenticate their download.

Django packages exist for this, such as django-downloadview, but it's not clear how (or if) the built in DRF authentication can interact with this. For example, these views don't interact with REST_FRAMEWORK 'DEFAULT_AUTHENTICATION_CLASSES'.

Does DRF have a solution for efficient media file serving that links into its authentication system? If so, I'd be happy to open a PR to clarify the docs.

Most helpful comment

I was able to patch everything together. In case this is useful to others, here is a minimal reproduction:

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)

All 9 comments

I don't think DRF has anything built in for this – django-downloadview's efficiency bits seems like it wouldn't be too hard to hook up to DRF.

This sort of thing is what Nginx's X-Accel is for.

https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/

Authenticate as normal in Django or DRF then have nginx serve the file.

DRF has no default for it for it is outside the scope of DRF.
django-downloadview looks promising but doesn't look like it supports DRF. It would probably be a good addition there.

Thank you for the responses.

The specific challenges I'm facing when I am trying to integrate django-downloadview (which supports Nginx X-Accel) and DRF is that django-downloadview does not integrate with DRF's 'DEFAULT_AUTHENTICATION_CLASSES'. So the 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.

You could delegate to django-downloadview in your DRF viewset's response phase, so have DRF do everything else such as authentication and so on. Looks like BaseDownloadView could be easily extended for that...

I was able to patch everything together. In case this is useful to others, here is a minimal reproduction:

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)

Nice example @johnthagen.

I'm going to close this as I think it's probably out of scope for DRF itself. Thanks.

This helped me a lot! Thanks a lot @johnthagen I am using django along with apache x-sendfile and have a complex permissions system to validate through before serving the file. Yet to implement but this looks very promising

Was this page helpful?
0 / 5 - 0 ratings