dfunckt / django-rules

Awesome Django authorization, without the database

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Adding Django-Rules to django app breaks logout endpoint, even though it doesn't use Django-rules permissions

Routhinator opened this issue · comments

This is a bit weird, and after debugging for a while, I can say that django-rules is causing this, but I am not sure how or why.

I have the following ModelViewSet in DRF that does not implement any django-rules. All endpoints work as expected with django-rules installed EXCEPT logout. Django rules is used in other apps in my django site, like writings and comments for object permissions, but the members app does not use it at all.

When someone (anyone) tries to logout, they are rejected with a 403, stating "You do not have permission to perform this action" - which is a message from the DRF permission system, however the same user is able to retrieve their user object from the /data/ endpoint, which uses the same settings for DRF permissions.

class UserViewSet(ModelViewSet):
    """
    The User endpoint for settings, authentication and anything else.
    """
    authentication_classes = DEFAULT_AUTH
    permission_classes = (IsAdminUser,)
    serializer_class = UserSerializer
    queryset = Member.objects.all()

    @action(methods=['get', 'post'], detail=False, permission_classes=[IsAuthenticated],
            serializer_class=UserSerializer, authentication_classes=[TokenAuthentication],
            url_path='data', url_name='data')
    def data(self, request):
        """
        Logged in users data retrieval + token refresh and user update endpoint.
        :param request: DRF request object
        :return: DRF Response object
        """
        if request.method == 'GET':
            data = UserSerializer(self.request.user, context=self.get_serializer_context()).data
            return Response({
                "user": data,
            }, status=HTTP_200_OK)

        if request.method == 'POST':
            serializer = self.get_serializer(request.user, data=request.data, partial=True)
            serializer.is_valid(raise_exception=True)
            user = serializer.save()
            data = UserSerializer(user, context=self.get_serializer_context()).data
            data['token'] = AuthToken.objects.create(user)
            return Response({
                "user": data,
            }, status=HTTP_200_OK)

        return Response({"message": "Unsupported action"}, status=HTTP_404_NOT_FOUND)

    @action(methods=['post'], detail=False, permission_classes=[CSRFPermission],
            serializer_class=UserRegistrationSerializer, authentication_classes=[SessionAuthentication],
            url_path='register', url_name='register')
    def register(self, request):
        """
        Accept and verify userdata, save and return user with AuthToken if valid.

        :param request: DRF request object
        :return: DRF Response object
        """
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.create(serializer.validated_data)
        data = UserSerializer(user, context=self.get_serializer_context()).data
        data['token'] = AuthToken.objects.create(user)
        return Response({
            "user": data,
        }, status=HTTP_200_OK)

    @action(methods=['post'], detail=False, permission_classes=[CSRFPermission],
            serializer_class=UserLoginSerializer, authentication_classes=[SessionAuthentication],
            url_path='login', url_name='login')
    def login(self, request):
        """
        Verify the login request and return a user object and a token if valid.

        :param request: Request object generated by Django REST Framework
        :return: user info and token when successful
        """
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data
        data = UserSerializer(user, context=self.get_serializer_context()).data
        data['token'] = AuthToken.objects.create(user)
        return Response({
            "user": data,
        }, status=HTTP_200_OK)

    @staticmethod
    @action(methods=['post'], detail=True, permission_classes=[IsAuthenticated],
            authentication_classes=[TokenAuthentication], url_path='logout', url_name='logout')
    def logout(request):
        # pylint: disable=W0212
        """
        Accept token and expire related session if valid.
        :param request: DRF request object
        :return: DRF response object
        """
        request._auth.delete()
        return Response(None, status=HTTP_204_NO_CONTENT)

If I strip rules from the other apps, this works as expected again. Put rules back in, and it breaks.

Relevant settings:


AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'theden_django.apps.core.exceptions.core_exception_handler',
    'NON_FIELD_ERRORS_KEY': 'error',
    'DEFAULT_PERMISSION_CLASSES': (),
    'DEFAULT_PAGINATION_CLASS': 'theden_django.apps.core.serializers.EnhancedPageNumberPagination',
    'DEFAULT_RENDERER_CLASSES': DEFAULT_RENDERER_CLASSES,
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
    ),
    'PAGE_SIZE': 10,
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.FormParser',
        'rest_framework.parsers.MultiPartParser',
    ),
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
}


Potentially I've missed something here. If I implement django-rules, does this force me to use it everywhere in my app?

Thinking this may have something to do with https://github.com/escodebar/django-rest-framework-rules rather than rules directly, so will cross post for now.

Implementing rules permissions on the endpoint does work, but it's odd that having it implemented on other endpoints breaks DRF permissions on this one.

Apparently all this behaviour was caused by having detail=True on the action decorator, which is odd. I think I either misunderstand the use of that flag on the new action decorator from DRF, or there's a bug in DRF with this.. but setting detail=False fixes all the extra actions that were not working.

Hi @Routhinator list_route and detail_route were condensed into action, where the detail argument determines whether or not the action is for list or detail views.

That all said, the logout action is broken.

  • It's missing the initial self argument
  • It's missing a third argument that should accept the instance id/pk.

@rpkilby Actually as I stated the logout action works fine now that I set detail=False with permissions. Without the permissions decorator, it works perfectly with detail=True. But ultimately the setting is irrelevant for this action in my app as it has no get method.

Also the action does not have self as it is a static method and does not need or use self in any way. Pylint is very clear that it doesn't need self. I could pass self and request but then there would be unused vars.