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()
Related
I looked through similar questions on the same topic and I think I am following all the rules specified for has_object_permission.
This is what I have in my settings.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
'users.permissions.CanAccessData', # this is my custom class
],
...
}
This is my permission class
class CanAccessData(permissions.BasePermission):
message = 'You do not have permission to perform this action.'
def has_permission(self, request, view):
print "has_permission`"
return True
def has_object_permission(self, request, view, obj):
print "has_object_permission"
return False
Here is my view structure:
class CompleteList(generics.ListCreateAPIView):
permission_classes = (CanAccessData,)
serializer_class = SomeSerializer
model = Some
filter_backends = (filters.OrderingFilter, filters.SearchFilter)
ordering_fields = (tuple of Some fields)
search_fields = ordering_fields
ordering = ('-create_date')
Still, has_object_permission is not getting called, has_permission gets called though.
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.
I ran into the same problem. The has_object_permission function is never called when listing objects.
Even if the following may not be the most efficient solution, you can override the list method in your view as follows, which is how I solved it for me:
from typing import List
import rest_framework.permissions as drf_permissions
def list(self, request, *args, **kwargs):
# base query set
queryset: QuerySet = self.filter_queryset(self.get_queryset())
# check object permissions for each object individually
valid_pks: List[int] = [] # # storage for keys of valid objects
permissions: List[drf_permissions.BasePermission] = self.get_permissions()
for obj in queryset:
for permission in permissions:
if permission.has_object_permission(request, self, obj):
valid_pks.append(obj.pk)
# remove not valid objects from the queryset
queryset = queryset.filter(pk__in=valid_pks)
# ... business as usual (original code)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
This basically is the original implementation except that it prefetches the relevant objects and checks the permission individually.
However, this may be DRY in the sense that you don't have to override the get_queryset() method to somehow reinvent your has_object_permission logic. But also it is slow, since it fetches objects twice. You could improve that situation by working with the already prefetched objects instead of filtering the queryset, though.
has_object_permission method will only be called if the view-level has_permission checks have already passed.
look this real-world example
olympia project on github (https://github.com/mozilla/addons-server/blob/master/src/olympia/ratings/permissions.py)
class CanDeleteRatingPermission(BasePermission):
"""A DRF permission class wrapping user_can_delete_rating()."""
def has_permission(self, request, view):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return user_can_delete_rating(request, obj)
when use this permission class, if has_permission is passed then has_object_permission ( user_can_delete_rating() function ) is called.
I am confused with the BasePermission in Django-rest-framework.
Here I defined a class: IsAuthenticatedAndOwner.
class IsAuthenticatedAndOwner(BasePermission):
message = 'You must be the owner of this object.'
def has_permission(self, request, view):
print('called')
return False
def has_object_permission(self, request, view, obj):
# return obj.user == request.user
return False
Using in views.py
class StudentUpdateAPIView(RetrieveUpdateAPIView):
serializer_class = StudentCreateUpdateSerializer
queryset = Student.objects.all()
lookup_field = 'pk'
permissions_classes = [IsAuthenticatedAndOwner]
But it doesn't work at all. Everyone can pass the permission and update the data.
The called wasn't printed.
And I used to define this class: IsNotAuthenticated
class IsNotAuthenticated(BasePermission):
message = 'You are already logged in.'
def has_permission(self, request, view):
return not request.user.is_authenticated()
It works well in the function
class UserCreateAPIView(CreateAPIView):
serializer_class = UserCreateSerializer
queryset = User.objects.all()
permission_classes = [IsNotAuthenticated]
So, what are the differences between the examples above, and function has_object_permission & has_permission?
We have following two permission methods on BasePermission class:
def has_permission(self, request, view)
def has_object_permission(self, request, view, obj)
Those two different methods are called for restricting unauthorized users for data insertion and manipulation.
has_permission is called on all HTTP requests whereas, has_object_permission is called from DRF's method def get_object(self). Hence, has_object_permission method is available for GET, PUT, DELETE, not for POST request.
In summary:
permission_classes are looped over the defined list.
has_object_permission method is called after has_permission method returns value True except in POST method (in POST method only has_permission is executed).
When a False value is returned from the permission_classes method, the request gets no permission and will not loop more, otherwise, it checks all permissions on looping.
has_permission method will be called on all (GET, POST, PUT, DELETE) HTTP request.
has_object_permission method will not be called on HTTP POST request, hence we need to restrict it from has_permission method.
Basically, the first code denies everything because has_permission return False.
has_permission is a check made before calling the has_object_permission. That means that you need to be allowed by has_permission before you get any chance to check the ownership test.
What you want is:
class IsAuthenticatedAndOwner(BasePermission):
message = 'You must be the owner of this object.'
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return obj.user == request.user
This will also allow authenticated users to create new items or list them.
I think this can help:
class IsAuthorOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Read-only permissions are allowed for any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the author of a post
return obj.user == request.user
has_permission() is a method on the BasePermission class that is used to check if the user has permission to perform a certain action on the entire model. For example, you might use it to check if a user has permission to view a list of all objects of a certain model.
has_object_permission() is a method on the BasePermission class that is used to check if the user has permission to perform a certain action on a specific instance of the model. For example, you might use it to check if a user has permission to view, update or delete a specific object of a certain model.
For example, you might have a Book model and a User model in your application. You could use has_permission() to check if a user has permission to view a list of all books, while you use has_object_permission() to check if a user has permission to view, update or delete a specific book.
class IsBookOwnerOrAdmin(permissions.BasePermission):
def has_permission(self, request, view):
# Check if the user is authenticated
if not request.user.is_authenticated:
return False
# Allow access for superusers
if request.user.is_superuser:
return True
# Allow access if the user is the owner of the book
if request.method in permissions.SAFE_METHODS:
return True
return False
def has_object_permission(self, request, view, obj):
# Allow access for superusers
if request.user.is_superuser:
return True
# Allow access if the user is the owner of the book
return obj.owner == request.user
As far as I can see, you are not adding your custom permission to the class as an argument.
This is your code:
class StudentUpdateAPIView(RetrieveUpdateAPIView):
serializer_class = StudentCreateUpdateSerializer
queryset = Student.objects.all()
lookup_field = 'pk'
permissions_classes = [IsAuthenticatedAndOwner]
But it should be:
class StudentUpdateAPIView(RetrieveUpdateAPIView, IsAuthenticatedAndOwner):
serializer_class = StudentCreateUpdateSerializer
queryset = Student.objects.all()
lookup_field = 'pk'
permissions_classes = [IsAuthenticatedAndOwner]
Note the custom permission IsAuthenticatedAndOwner as an argument in the class header.
PS: I hope this helps, I am a beginner in DRF but this is one of the things I just learned.
I am implementing an API where I have nested structures.
Lets say it is a zoo and I can call GET /api/cage/ to get a list of cages GET /api/cage/1/ to get cage ID 1, but then I can GET /api/cage/1/animals/ to get a list of animals in that cage.
The problem I am having is with permissions. I should only be able to see animals in the cage if I can see the cage itself. I should be able to see the cage itself if has_object_permission() returns True in the relevant permission class.
For some reason, has_object_permission() gets called when I do GET /api/cage/1/, but has_permission() gets called when I call GET /api/cage/1/animals/. And with has_permission() I don't have access to the object to check the permissions. Am I missing something? How do I do this?
My cage viewset looks more or less like this
class CageViewSet(ModelViewSet):
queryset = Cage.objects.all()
serializer_class = CageSerializer
permission_classes = [GeneralZooPermissions, ]
authentication_classes = [ZooTicketCheck, ]
def get_queryset(self):
... code to only list cages you have permission to see ...
#detail_route(methods=['GET'])
def animals(self, request, pk=None):
return Request(AnimalSerializer(Animal.objects.filter(cage_id=pk), many=True).data)
My GeneralZooPermissions class looks like this (at the moment)
class GeneralZooPermissions(BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return request.user.has_perm('view_cage', obj)
It seems like this is a bug in DRF. Detailed routes do not call the correct permission check. I have tried reporting this issue to DRF devs, but my report seems to have disappeared. Not sure what to do next. Ideas?
The issue I posted with DRF is back and I got a response. Seems like checking only has_permission() and not has_object_permission() is the intended behavior. This doesn't help me. At this point, something like this would have to be done:
class CustomPermission(BasePermission):
def has_permission(self, request, view):
"""we need to do all permission checking here, since has_object_permission() is not guaranteed to be called"""
if 'pk' in view.kwargs and view.kwargs['pk']:
obj = view.get_queryset()[0]
# check object permissions here
else:
# check model permissions here
def has_object_permission(self, request, view, obj):
""" nothing to do here, we already checked everything """
return True
OK, so after reading a bunch of DRF's code and posting an issue at the DRF GitHub page.
It seems that has_object_permission() only gets called if your view calls get_object() to retrieve the object to be operated on.
It makes some sense since you would need to retrieve the object to check permissions anyway and if they did it transparently it would add an extra database query.
The person who responded to my report said they need to update the docs to reflect this. So, the idea is that if you want to write a custom detail route and have it check permissions properly you need to do
class MyViewSet(ModelViewSet):
queryset = MyModel.objects.all()
....
permission_classes = (MyCustomPermissions, )
#detail_route(methods=['GET', ])
def custom(self, request, pk=None):
my_obj = self.get_object() # do this and your permissions shall be checked
return Response('whatever')
If you want to define permissions while doing another method that doesn't call the get_object() (e.g. a POST method), you can do overriding the has_permission method. Maybe this answer can help (https://stackoverflow.com/a/52783914/12737833)
Another thing you can do is use the check_object_permissions inside your POST method, that way you can call your has_object_permission method:
#action(detail=True, methods=["POST"])
def cool_post(self, request, pk=None, *args, **kwargs):
your_obj = self.get_object()
self.check_object_permissions(request, your_obj)
In my case I didn't address requests correctly so my URL was api/account/users and my mistake was that I set URL in frontend to api/account/ and thats not correct!
Using the Django REST framework, I use this view and permission to allow only project owners to get their projects.
view.py
class ProjectViewSet(viewsets.ModelViewSet):
permission_classes = (
IsProjectOwner,
permissions.IsAuthenticated,
)
def get_queryset(self):
return Project.objects.filter(owner=self.request.user)
permissions.py
class IsProjectOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
When an user tries to get a project which does not belong to him, a HTTP 404 arises. However, I would like to get HTTP 403_Forbidden. Here is the test I use
def test_auth_get(self):
self.client.credentials(
HTTP_AUTHORIZATION=self.authenticated_user_token
)
response = self.client.get(
'/-/projects/%s/' % self.project_owner_project_id
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
I tried to solve the problem using a get_object() method like in the REST docu http://www.django-rest-framework.org/api-guide/permissions/#object-level-permissions. But I am not sure how to check the permission before knowing the actual object.
Here you need to overide your get_queryset method;
Actually you view find object from queryset you are passing from get_queryset method.
def get_queryset(self):
if self.action == 'update':
return Project.objects.filter(owner=self.request.user)
else:
return Project.objects.all()
The get_queryset method will already limit the Project list to the ones where is owner is the current user. This will work for lists but also to retrieve or update a project.
You don't need a permission on top of that.
I have created a permissions file for the isOwnerOrReadOnly permission but the has_object_permission function is not being called at all (I have place a print statement there to check).
This is how I am using this permission in my view:
class CarDetail(generics.RetrieveUpdateDestroyAPIView):
.....
serializer_class = car_serializers.CarSerializer
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsOwnerOrReadOnly,)
What am I missing?
#adeleinr I am guessing you have declared your own get_object method( i would have asked you this in the comment but don't have sufficient points to do that :D), in that case you have to use check_object_permissions in the get_object ( also in PUT, DELETE ) .Use this in your get_object
obj = get_object_or_404(queryset, **filter)
self.check_object_permissions(self.request, obj)
I was inspired by article How I could delete any video on YouTube
and wanted to check if in my django project everything is working safe, and ended up here.
This is pretty important question!
And the answer is very good.
The Django Rest Framework makes false impression that everything is working fine, when one looks at it through browsable API view.
Object, which authenticate user owns:
Object, which authenticate user does NOT owns:
Hidden DELETE button makes you feel, that everything is fine.
You are authenticated, delete button hidden.
Cool! You stay unaware until you test it wit CURL or some other tool and notice this huge security hole.
Django is sometimes too much magic....
Example:
views.py
#authentication_classes((ExpiringTokenAuthentication, SessionAuthentication))
#permission_classes((IsOwnerOrReadOnly, ))
class UserFavouritesSpotDetail(RetrieveUpdateDestroyAPIView):
model = UsersSpotsList
serializer_class = FavouritesSpotsListSerializer
def get_queryset(self):
queryset = UsersSpotsList.objects.filter(
role=1)
return queryset
def get_object(self):
queryset = self.get_queryset()
obj = get_object_or_404(
queryset,
pk=self.kwargs['pk'],
role=1)
self.check_object_permissions(self.request, obj)
return obj
Notice the crucial line mentioned by Shivansh:
self.check_object_permissions(self.request, obj)
When I was missing it the vulnerability was existing.
permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `user` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
return obj.user == request.user
TEST it e.g with http://www.getpostman.com/
provide Token of user not owning the object.
if everything is fine you should see "detail": "You do not have permission to perform this action."