DRF requires the user to be logged in to use login_user api

48 views Asked by At

I am trying to create a CustomUserViewset, and add a login_user api to it. The problem is that although I set the permission_classes to AllowAny, still when calling the login_user api, it says: {"detail":"Please login to perform this action"}.

Here is my API:

class CustomUserViewset(AutoPermissionViewSetMixin, viewsets.ModelViewSet):
    queryset = User.objects.none()
    serializer_class = CustomUserSerializer

    permission_type_map = {
        "create": "add",
        "destroy": "delete",
        "partial_update": "change",
        "retrieve": "view",
        "update": "change",
        "register": "view",
        "login_user": "view",
        "logout": "change",
    }

    @decorators.action(methods=["POST"], detail=False, permission_classes=[permissions.AllowAny])
    def login_user(self, request):
        serializer = LoginSerializer(data=request.data)

        if not serializer.is_valid():
            raise exceptions.ValidationError({"detail": "Invalid username or password"})

        username = serializer.validated_data["username"]
        password = serializer.validated_data["password"]

        user = authenticate(request, username=username, password=password)

        if user is not None:
            login(request, user)
            return Response(CustomUserSerializer(user).data, status=status.HTTP_200_OK)
        else:
            raise exceptions.AuthenticationFailed({"detail": "Invalid username or password"})

As you see, I have permission_classes=[permissions.AllowAny] in the api action. Also, giving this permission class in the action was the last thing I tried, before that, I tried to adjust the permission in rules.py:

import typing

import rules

if typing.TYPE_CHECKING:
    from .models import User
rules.add_perm("accounts.login_user", rules.predicates.always_allow)

None of the above methods has worked, and I still get the same message that I need to log in to perform this action.

2

There are 2 answers

0
Vahid On BEST ANSWER

**** UPDATE ON THE ANSWER ****

I solved this problem with first adding a few permissions to permission_type_map, then adjusting the rules.py:

class CustomUserViewset(AutoPermissionViewSetMixin, viewsets.ModelViewSet):
    queryset = User.objects.none()
    serializer_class = serializers.CustomUserSerializer

    permission_type_map = {
        "list": "list",
        "create": "add",
        "destroy": "delete",
        "partial_update": "change",
        "retrieve": "view",
        "update": "change",
        "register": "register",
        "login": "login",
        "logout": "logout",
    }

    @decorators.action(methods=["post"], detail=False)
    def register(self, request):
        register_serializer = serializers.RegisterSerializer(data=request.data)

        register_serializer.is_valid(raise_exception=True)

        username = register_serializer.validated_data["username"]
        password = register_serializer.validated_data["password"]
        # email = register_serializer.validated_data["email"] // for now, email is auto-generated
        email = f"{username}@example.com"

        new_user = User.objects.create_user(username=username, email=email, password=password)
        user_serializer = serializers.CustomUserSerializer(new_user)
        return Response(user_serializer.data, status=status.HTTP_201_CREATED)

    @decorators.action(methods=["post"], detail=False)
    def login(self, request):
        login_serializer = serializers.LoginSerializer(data=request.data)

        login_serializer.is_valid(raise_exception=True)

        username = login_serializer.validated_data["username"]
        password = login_serializer.validated_data["password"]

        user = authenticate(request, username=username, password=password)

        if user:
            login(request, user)
            return Response(
                serializers.CustomUserSerializer(user).data,
                status=status.HTTP_200_OK,
            )
        raise exceptions.AuthenticationFailed({"detail": "Invalid username or password"})

    @decorators.action(detail=False, methods=["get"], permission_classes=[permissions.IsAuthenticated])
    def logout(self, request):
        logout(request)
        return Response({"detail": "Successfully logged out"})

And in the rules.py:

import typing

import rules

if typing.TYPE_CHECKING:
    from .models import User


@rules.predicate(bind=True)
def is_active(self, user: "User"):
    return getattr(user, "is_active", False)


@rules.predicate(bind=True)
def is_admin(self, user: "User"):
    return is_active(user) if user.is_superuser else False


@rules.predicate(bind=True)
def is_staff(self, user: "User"):
    return is_active(user) if user.is_staff or user.is_superuser else False


@rules.predicate(bind=True)
def is_same_user(self, auth_user: "User", target_user: "User"):
    """
    Is authenticated user the same user as target_user?
    """
    if auth_user and target_user:
        print("auth_user:", auth_user)
        print("target_user:", target_user)
        return auth_user.pk == target_user.pk
    return False


rules.add_perm("accounts.view_user", is_admin | is_same_user)
rules.add_perm("accounts.list_user", is_active)
rules.add_perm("accounts.change_user", is_admin | is_same_user)
rules.add_perm("accounts.add_user", is_admin)
rules.add_perm("accounts.delete_user", is_admin)
rules.add_perm("accounts.login_user", rules.predicates.always_allow)
rules.add_perm("accounts.register_user", rules.predicates.always_allow)
rules.add_perm("accounts.logout_user", is_active)

Now with these new permissions and rules, all three APIs work as expected.

2
Hantsaniala Eléo On

Maybe the decorator is not overriding it properly, but I can't confirm it. But here is another alternative you can use. The following example separate the login to another class so that access permission is not mixed up and it only handle one thing.

First, create custom viewset to handle this login only:

from rest_framework import status, viewsets
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from custom_user.models import CustomUser
from custom_user.serializers import CustomUserSerializer

class CustomUserLoginViewSet(viewsets.ViewSet):
    serializer_class = CustomUserLoginSerializer
    permission_classes = [AllowAny]

    def post(self, request, *args, **kwargs):
        data = request.data
        if CustomUser.objects.filter(username=data["username"]).exists():
            user: CustomUser = CustomUser.objects.get(
                username=data["username"])
            if user.check_password(data["password"]):
                refresh = RefreshToken.for_user(user)
                return Response(
                    {
                        "data": {
                            "user": CustomUserSerializer(user).data,
                            "token": str(refresh.access_token),
                            "refresh": str(refresh),
                        }
                    },
                    status=status.HTTP_200_OK,
                )
            else:
                return Response(
                    {"message": "Invalid Password"}, status=status.HTTP_400_BAD_REQUEST
                )
        else:
            return Response(
                {"message": "User Does Not Exist"}, status=status.HTTP_400_BAD_REQUEST
            )

Then manualy call it inside your project/urls.py like this:

# ... other import
from django.urls import include, path
from custom_user.views import CustomUserLoginViewSet

urlpatterns = [
    # ... other path
    path("login/", CustomUserLoginViewSet.as_view({"post": "post"})),
    # ... other path
]

Do not forget to update your import path or the core code of your login because mine is with djangorestframework-simplejwt. It's just an example but it's working.