Different authentications and permissions in ModelViewSet - Django REST framework - django

This question is similar to this one: Using different authentication for different operations in ModelViewSet in Django REST framework, but it didn't work for me.
I've got the following viewset:
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = UserProfile.objects.none()
permission_classes = [SpecialPermission]
SpecialPermission looks like this:
class SpecialPermission(IsAuthenticated):
def has_permission(self, request, view):
if request.method == 'POST':
return True
return super().has_permission(request, view)
REST framework settings:
"DEFAULT_AUTHENTICATION_CLASSES": ["backend.api.authentication.ExpiringTokenAuthentication"],
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
I want to everybody to be able to post to UserViewSet but every other method should require Authentication. However, with the code above I get an Unauthorized Response on post.
What do I need to change?

Although it can be done, this requirement imo does not justify this ifology as auth/user related stuff should be clean and secure.
Instead extract POST method from this viewset to its own class.
class UserViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
serializer_class = UserSerializer
queryset = UserProfile.objects.none()
permission_classes = [SpecialPermission]
class CreateUserView(CreateAPIView):
serializer_class = UserSerializer
queryset = UserProfile.objects.none()
authentication_classes = []
if you really want to disable authentication in this viewset I'd rather recommend this
def get_authenticators(self):
if self.action == 'create':
return []
return super().get_authenticators()
That's more explicit than your solution.

I figured it out: Making perform_authentication lazy solved my problem. Now I can post but authentication still runs on all other methods where it is needed.
def perform_authentication(self, request):
"""
Perform authentication on the incoming request.
Note that if you override this and simply 'pass', then authentication
will instead be performed lazily, the first time either
`request.user` or `request.auth` is accessed.
"""
pass

Related

Understanding permissions in Django

I trying to understand permissions in Django. In the points below I wrote what I was able to find on this issue and how I understand it. Maybe someone will be able to tell me if I am thinking correctly or if it works in a different way. I will be grateful for additional articles on this topic. Thanks!
1. APIView with built-in permissions
Django has some built-in permissions that we can use in APIView through permission_classes.
class OrderView(APIView):
permission_classes = [permissions.IsAuthenticated]
We can add the following built-in permissions to permission_classes, because they don’t have has_object_permission:
AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly, IsAdminUser, DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly
2. APIView with custom permissions
Let’s say that we want to use the custom permission from the point 1 in APIView.
Can we use permission_classes in that case?
class OrderView(APIView):
permission_classes = [permissions.IsAuthenticatedAndOwner]
I don’t know which object will be sent to has_object_permission and how to do it. Is it a good way?
def has_object_permission(self, request, view, obj): # <-- obj
return obj.user == request.user
3. APIView with check_permissions and check_object_permissions.
I am a little confused about the methods. If I understood correctly these methods are used to call has_permission and has_object_permission from APIView? Is there a different reason to use it? Do we use it only with APIView?
4. ViewSet with custom permissions
Let’s say that we want to use the custom permission from the point 1 but with ViewSet
class MessageViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedAndOwner]
queryset = Message.objects.all()
serializer_class = MessageSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
In that case what will be sent as obj to has_object_permission?
def has_object_permission(self, request, view, obj): # <-- obj
return obj.user == request.user
Will it be data from queryset?

Django Rest Framework: Update and delete current user without providing pk

I know a lot of people already asked about handling with current user but I couldn't find solution so I post this.
What I want to do is to get, put and delete current user without providing pk.
I want to set endpoint like users/my_account
My current code is here
class MyAccountDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CustomUser.objects.all()
serializer_class = UserSerializer
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
And now I can get current user's info but when I try to update or delete the current user,
AssertionError: Expected view MyAccountDetail to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly.
How can I solve this?
Update
urlpatterns = [
path('users/my_account', views.MyAccountDetail.as_view()),
]
In this case, you will need to override get_object() method in your MyAccountDetail view. For example:
from rest_framework.permissions import IsAuthenticated
class MyAccountDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CustomUser.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user
You need to do that, because by default get_object method looks for lookup_url_kwarg or lookup_field in the URL, and from that it will try to fetch the object using pk or whatever you have configured in lookup_field or lookup_url_kwarg.
FYI, I have added a permission class as well, because without it, self.request.user will be an anonymous user, hence will throw error.

django rest permissions allow both IsAdmin and custom permission

I have a views.py as below,
from webapi.permissions import IsOwner
class MemberDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = members.objects.all()
serializer_class = MemberSerializer
permission_classes = (permissions.IsAdminUser,IsOwner)
And the following is custom permission to check if the user is ower of object in webapi.permissions,
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
Now the issue is it is check if he is a admin user and gives permissions to update / delete, But if the owner is the user it should actually give permission to edit he data but in this case it is failing.
On seeing the question "Django Rest Framework won't let me have more than one permission" I tried as below also still it did not work when I use Or,
class MemberDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = members.objects.all()
serializer_class = MemberSerializer
permission_classes = (Or(permissions.IsAdminUser,IsOwner))
If I use Or it is failing with error during run time as "'Condition' object is not iterable"
Since DRF 3.9, you can use the logical bitwise operators | and & (~ was added in 3.9.2).
As outlined in the docs you would just need
permission_classes = (permissions.IsAdminUser|IsOwner,)
If you need give edit permissions for admin and owner users only, you can implement custom permission class:
class IsOwnerOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user or request.user.is_admin
And use only this one in the view:
permission_classes = [IsOwnerOrAdmin]
This method is documented here.
you are using tuple syntax but you need comma to act as a tuple
replace
permission_classes = (Or(permissions.IsAdminUser,IsOwner))
with
permission_classes = (Or(permissions.IsAdminUser,IsOwner), )

Django REST - Deny read access to ListAPIView

I'm trying to familiarize myself with djangorestframework's permissions setup. As part of that, I'm trying to establish a proof-of-concept for denying all access (incl. read access) based on a function's response and making sure it gets denied. (Basically making sure the function gets used when I think it's supposed to get used.) I've got a RetrieveAPIView and a ListAPIView set up identically. The Retrieve denies access as it should, but the List allows access, and I'm trying to figure out why there is a difference.
# in views.py
class TableList(generics.ListAPIView):
queryset = MyModel.objects.all()
serializer_class = MySerializer
permission_classes = (RejectAll,)
class TableDetail(generics.RetrieveAPIView):
queryset = MyModel.objects.all()
serializer_class = MySerializer
permission_classes = (RejectAll,)
# in permissions.py
class RejectAll(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return False # for proof of concept -- this should always block all access?
# in settings.py
...
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
# in serializers.py
class MySerializer(serializers.ModelSerializer):
class Meta:
model = SolarData
fields = (...)
I understand that I can create a get_object() method that calls self.check_object_permissions(self.request, obj), but wouldn't that defeat the purpose of having permission_classes set? Shouldn't the generic view perform that same check on its own?
edit/clarification: By read access I of course mean GET method.
Found the answer right after posting this. Apparently there's also a has_permission() function separate from the has_object_permission() that I used. So permissions.py becomes:
# permissions.py
class RejectAll(permissions.BasePermission):
def has_permission(self, request, view):
return False
This works as expected now.

Using different authentication for different operations in ModelViewSet in Django REST framework

I have the following ModelViewSet
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
authentication_classes = (TokenAuthentication,)
permission_classes = (permissions.IsAuthenticated, MyUserPermissions)
I want the create method (POST on /users/) to not ask for any authentication. How can I override the authentication_classes in this case? I'm talking about ModelViewSet not generic API views.
I want the create method (POST on /users/) to not ask for any authentication.
Actually that's not quite what you want. You want POST on users to not require any permissions, which will have the effect that either authenticated or unauthenticated requests will succeed.
I'd suggest overriding your permission classes so that they always allow POST requests. Follow the custom permissions documentation for more info on that.
Essentially you'll have something like:
class IsAuthenticatedOrCreate(permissions.IsAuthenticated):
def has_permission(self, request, view):
if request.method == 'POST':
return True
return super(IsAuthenticatedOrCreate, self).has_permission(request, view)
And probably something similar for your other permission class too.