WhyNotTryCalmer
WhyNotTryCalmer

Reputation: 496

How to update 'Allow' headers in DRF to match an OPTIONS request's permissions?

I am using Django Rest Framework 3.14 with ModelViewsets and a settings-wide DjangoModelOrAnonReadOnly permission class.

Given this config, out of the box my JSON API seems to respond to OPTIONS requests in a misleading way, i.e. sending unauthenticated OPTIONS requests to /api/collections/collectionA/items is replied with Allow: GET, POST, HEAD, OPTIONS in the headers (correct would be: GET, HEAD, OPTIONS). However if I define my own metadataclass and do something like:

def options(self, request, *args, **kwargs) -> response.Response:
    allowed_actions = self.metadata_class().determine_actions(request, self)
    allowed_actions = ", ".join(allowed_actions.keys())
    # ^ allowed_actions is correct 
    data = self.metadata_class().determine_metadata(request, self)
    return response.Response(data, headers={"Allow": allowed_actions})

I am able to get the correct allowed_actions (GET, OPTIONS, HEAD). However, and that is my issue, headers are unmodified by the last statement in the snipper above.

How can I update my headers to ensure that the Allow headers correctly reflect the state of my API?


Context: this is required while implementing an OGC API Features endpoint. It's an OpenAPI defintion for Geospatial data. Details can be found here.

And from the part 4 (CRUD operations):

A server is not required to implement every method described in this specification (i.e. POST, PUT, PATCH or DELETE) for every mutable resource that it offers. Furthermore, a server that supports the ability to add, modify or remove resources from collections is not likely to be an open server. That is, access to the server, and specifically the operations that allow resource creation, modification and/or removal, will be controlled. Such controls might, for example, take the form of policy requirements (e.g. resources on this server can be inserted or updated but not deleted) or user access control requirements (e.g. user "X" is only allowed to create resources but not update or delete resources). Regardless of the controls the server must be able to advertise, within the control context in place, which methods are available for each resource that it offers. This is accomplished using the HTTP OPTIONS method.

The HTTP OPTIONS method allows the server to explicitly declare which HTTP methods are supported for a particular resource endpoint. This specification deals with the HTTP POST, PUT, PATCH and DELETE methods but any relevant HTTP method may be listed for a particular resource.

Upvotes: 3

Views: 405

Answers (2)

Lennart
Lennart

Reputation: 11

The answer from JPG doesn't work for object permissions. I've extended it to do that:

from django.http import Http404
from rest_framework.generics import GenericAPIView, get_object_or_404
from rest_framework.request import Request, clone_request
from rest_framework.views import APIView
from rest_framework import exceptions as rest_exceptions

def view_is_object_view(view: APIView) -> bool:
    if not isinstance(view, GenericAPIView):
        return False
    
    lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field
    if not lookup_url_kwarg in view.kwargs:
        return False
    
    return True

def get_allow_list(request: Request, view: APIView) -> list[str]:
    allowed_methods = []
    for method in view.allowed_methods:
        view.request = clone_request(request, method)
        try:
            view.check_permissions(view.request)
            if view_is_object_view(view):
                queryset = view.filter_queryset(view.get_queryset())
                lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field

                filter_kwargs = {view.lookup_field: view.kwargs[lookup_url_kwarg]}
                obj = get_object_or_404(queryset, **filter_kwargs)
                view.check_object_permissions(view.request, obj)

            allowed_methods.append(method)
        except (rest_exceptions.APIException, Http404):
            pass

    return allowed_methods

class AllowHeaderMixin:
    def finalize_response(self, request, response, *args, **kwargs):
        response = super().finalize_response(request, response, *args, **kwargs)
        if request.method == "OPTIONS":
            allow_str = ", ".join(get_allow_list(request, self))
            response.headers["Allow"] = allow_str
        return response

Then add AllowHeaderMixin to your viewset classes.

Personally I would change a few more things to the above:

  • Use it for all requests instead of only OPTIONS requests, so that the Allow header is consistent.
  • Don't remove a method from the Allow header if a Http404 occurs, since a 404 may be temporary.

Upvotes: 0

JPG
JPG

Reputation: 88509

Update - 1

It seems to be you need to override the finalize_response(...) method to patch the Allow header.


from django.http import Http404
from rest_framework import permissions, viewsets
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework.request import clone_request

from polls.models import Poll

from .serializers import PollSerializer


def get_allow_list(request, view) -> list[str]:
    allowed_methods = []
    for method in view.allowed_methods:
        view.request = clone_request(request, method)
        try:
            view.check_permissions(view.request)
            allowed_methods.append(method)
        except (APIException, PermissionDenied, Http404):
            pass

    return allowed_methods


class PollViewSet(viewsets.ModelViewSet):
    serializer_class = PollSerializer
    queryset = Poll.objects.all()
    permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]

    def finalize_response(self, request, response, *args, **kwargs):
        response = super().finalize_response(request, response, *args, **kwargs)
        if request.method == "OPTIONS":
            allow_str = ", ".join(get_allow_list(request, self))
            response.headers["Allow"] = allow_str
        return response

I think you missunderstood the concept of Allow header

The Allow header lists the set of methods supported by a resource.

Since you are using a ModelViewsets, and I assume you are using it with a router, with all default configurations, then, most likely, you will have all the HTTP methods enabled for the client. In other words, the Allow header returns the value irrespective of the Authorization checks.

Upvotes: 4

Related Questions