Understanding permissions in Django - 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?

Related

Different authentications and permissions in ModelViewSet - Django REST framework

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

Django Rest Framework custom permission not working

I want users to have access only to the records that belong to them, not to any other users' records so
I've created the following view:
class AddressViewSet(viewsets.ModelViewSet):
authentication_classes = (TokenAuthentication,)
permission_classes = [IsAuthenticated, IsOwner]
queryset = Address.objects.all()
def retrieve(self, request, pk):
address = self.address_service.get_by_id(pk)
serializer = AddressSerializer(address)
return Response(serializer.data, status=status.HTTP_200_OK)
I want only the owner of the records to have access to all the methods in this view ie retrieve, list, etc (I'll implement the remaining methods later) so I created the following permissions.py file in my core app:
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print('here in has_object_permission...')
return obj.user == request.user
this wasn't working, so after going through stackoverflow answers I found this one Django Rest Framework owner permissions where it indicates that has_permission method must be implemented. But as you can see in that answer, it's trying to get the id from the view.kwargs but my view.kwargs contains only the pk and not the user. How can I fix this? Do I need to implicitly pass the user id in the request url? that doesn't sound right.
Here's the test I'm using to verify a user cannot access other user's records:
def test_when_a_user_tries_to_access_another_users_address_then_an_error_is_returned(self):
user2 = UserFactory.create()
addresses = AddressFactory.create_batch(3, user=user2)
address_ids = [address.id for address in addresses]
random_address_id = random.choice(address_ids)
url = reverse(self.ADDRESSES_DETAIL_URL, args=(random_address_id,))
res = self.client.get(url, format='json')
print(res.data)
Currently just using the test to check the data returned, will implement the assertions later on.
Edit
So I added has_permission method to IsOwner:
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
if I put a print statement here it gets printed, but doesn't seem to be hitting the has_object_permission method, none of the prints I added there are being displayed
This answer was the right one for me.
It says:
The has_object_permission is not called for list views. The
documentation says the following:
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you
require object-level filtering of list views, you'll need to filter
the queryset separately. See the filtering documentation for more
details.
Link to documentation
Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already passed.
You need to write the has_permission too in order to make your custom permission works.
Here is the official docs and mentioned it. It should works after you add in has_permission.
As mentioned in the docs, permissions are checked on self.get_object method call.
def get_object(self):
obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"])
self.check_object_permissions(self.request, obj)
return obj
Which basically is all retrieve method does in ModelViewSet
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
Whatever it is you do in self.address_service.get_by_id(pk) should either be moved to self.get_object or call self.check_object_permissions(self.request, obj) in retrieve method.
In the basic scenario this is all you need. There's no need to overwrite retrieve method.
class AddressViewSet(viewsets.ModelViewSet):
serializer_class = AddressSerializer
authentication_classes = (TokenAuthentication,)
permission_classes = [IsAuthenticated, IsOwner]
queryset = Address.objects.all()

Authentication on specific method for generic API views

I have used ListCreateAPIView and RetrieveUpdateDestroyAPIView for a model. Now I want to add JWT authentication to only the Update and Destroy part in the RetrieveUpdateDestroyAPIView. How can I do that?
Let me make my question a bit more clear. I have a model named Post. Now All users are allowed to view the post but update, delete is only available to the user who created it. And I want to use JWT Authentication.
You can write custom permission class for this:
from rest_framework import permissions
class CustomPermission(permissions.BasePermission):
def has_permission(self, request, view):
if view.action in ('update', 'destroy'):
return request.user.is_authenticated
return True
And use in in your view:
class ExampleView(RetrieveUpdateDestroyAPIView):
permission_classes = (CustomPermission,)
we can override the method get_authenticators and don't forget to add authentication_classes to api view.
def get_authenticators(self):
if self.request.method in ['PUT', 'DELETE']:
return [auth() for auth in self.authentication_classes]
else:
return []
For your question update we need to add object level permissions like below
class OwnerRequiredPermission(object):
def has_object_permission(self, request, obj):
return obj.created_by == request.user
add above permission class to permission_classes

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), )

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.