djangodjango-rest-framework

Django decorator and middleware issue


My decorator looks like this

def require_feature(feature_name):
    def decorator(view_func):
        print(f"process_view - view_func: {view_func}")  # Debugging

        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            return view_func(request, *args, **kwargs)

        _wrapped_view.required_feature = feature_name
        return _wrapped_view

    return decorator

and the middleware looks like this

class EntitlementMiddleware(MiddlewareMixin):
    def __init__(self, get_response) -> None:
        self.get_response = get_response

    def __call__(self, request) -> Any:
        if request.user.is_authenticated:
            request.user.features = get_user_features(request.user)
        else:
            request.user.features = []
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        print(f"process_view - view_func: {view_func}")  # Debugging
        required_feature = getattr(view_func, "required_feature", None)
        if required_feature and not request.user.features.filter(name=required_feature):
            raise PermissionDenied("You do not have access to this feature")

        return None  # if none is returned then view is called normally

    def process_response(self, request, response):
        return response

and this is how I am using it in my viewset

class MyViewSet(ValidatePkMixin, viewsets.ViewSet):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]
    queryset = My.objects.all()

    @method_decorator(require_feature("Basic"))
    def list(self, request):
      pass

the decorator sets the required_feature when server starts. the middleware gets called when I make a /get call but this

required_feature = getattr(view_func, "required_feature", None)

returns None

what am I missing here?


Solution

  • You are check from middleware function(method) dispatch which will be called from middleware. And you decorate completely other function(method) list.

    in Django, if you work with Generic CBVs or ViewSets request goes through middlewares, and through Class.as_view() the request-dispatcher will be returned. For both - for Generic CBVs and for ViewSets it is:

    class View:
        ...
        def dispatch(self, request, *args, **kwargs):
            return handler # exactly here will be called your cls.list 
        ...
    

    But right now i see, you want reinvent user permissions/permission classes.

    The best way for you goal is - to create child class from BasePermission. More here: https://www.django-rest-framework.org/api-guide/permissions/

    You want to catch all requests on middleware level. For that DRF offer you settings.DEFAULT_PERMISSION_CLASSES:

    #settings.py
    REST_FRAMEWORK = {
        'DEFAULT_PERMISSION_CLASSES': [
            'myapp.permissions.IsFeatured',
        ]
    }
    

    This class check "feature" by user for choosen handler. BTW I still don't understand, why you don't use custom user.permissions.

    After you need the permission class:

    #myapp/permissions.py
    from rest_framework.permissions import BasePermission
    
    class IsFeatured(BasePermission):
        def has_permission(self, request, view):
            handler = getattr(view, view.action)
            feature = getattr(handler, "required_feature", None)
            if feature:
                return request.user.features.filter(name=feature).exists()
            return True
    

    Thats all. It should work as you want, but i dont check it on errors. BTW, this solution repeated already existed solution to work with user.permission