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!
Related
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()
I implemented DRF as per the document. At one point I figured out, once the user is authenticated, the user is allowed to fetch data of any user in the systems.
I have implemented filtering as per this document.
I read through the permission document and could not find a way to filter out queryset based on the owner. In my one of the views, I am checking if the owner is same as the user who requested.
My question is, Do I have to do the same in all viewsets? or There is a general way where I can check this condition?
Not sure, if it is the best way, but I do it by overriding get_queryset
def get_queryset(self):
queryset = YOUR_MODEL.objects.filter(user_id=self.request.user.id)
return queryset
Doing it, using permisson class
class IsInUserHierarchy(permissons.BasePermission):
def has_permission(self, request, view):
return bool(isinstance(request.user, UserClassHierarchy))
Some explanations. IsInUserHierarchy class is very similar to IsAdminUser. It checks, if request.user is in the required class (import UserClassHierarchy from models), using simple python isinstance() method
Just create a permissions file, and add something like this:
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Instance must have an attribute named `owner`.
return obj.owner == request.user
Then, in your ViewSet, use this permission class:
class MyViewSet(viewsets.ViewSet):
permission_classes = (IsOwner,)
Now, just import your permissions file anywhere you want to use this logic and you don't have to duplicate any code
Old question but for anyone curious, you can still create follow the general procedure as outlined by Dalvtor and Django/DRF docs.
Your viewset makes a call to check the object through:
self.check_object_permissions(self.request, obj)
With your custom permission, you need to check if it is iterable and iterate and check each object in the queryset:
from rest_framework import permissions
from collections.abc import Iterable
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# For Get Queryset (List)
if isinstance(obj, Iterable):
for o in obj:
if o.user != request.user:
return False
# For Get Object (Single)
elif obj != request.user:
return False
return True
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 have a CRUD in django rest framework and to control the permissions I'm using permission classes. Here is the permission class I created for the CRUD. This works well for update and delete:
class LinkAssetPermission(permissions.BasePermission):
message = 'Link access not allowed.'
def has_object_permission(self, request, view, obj):
return obj.content.delivery.owner == request.user and obj.content.delivery.can_change
My Problem if that this class does not work well for my create view:
class AssetCreate(generics.CreateAPIView):
"""
Link a new asset
"""
queryset = Asset.objects.none()
serializer_class = AssetSerializer
permission_classes = (permissions.DjangoModelPermissions, LinkAssetPermission, )
When obj.content.delivery.can_change returns false I cannot create, update or delete model objects. The same for obj.content.delivery.owner == request.user . I can only create, update or delete when the user is the owner. All this works as I want for update and delete, just for create that it is not working. Probably because I don't have the object yet, that's the core of my question.
The object exists when creating and I have also tried to just return False at has_object_permission and I still can create the record.
Should I create a new permission class for the insert or should I change something in my view?
Thanks for any help
I found out what I had to do. Here is my new permission class:
class LinkAssetPermission(permissions.BasePermission):
message = 'Link access not allowed.'
def has_permission(self, request, view):
try:
content = Content.objects.get(pk=request.parser_context["kwargs"]["content_id"])
except:
return False
return content.delivery.owner == request.user and content.delivery.can_change
def has_object_permission(self, request, view, obj):
return obj.content.delivery.owner == request.user and obj.content.delivery.can_change
On create the method has_object_permission is not considered but the has_permission is. So I retrieve the parameter sent from the urls.py, query and check the properties of the object I want.
Thanks
has_object_permissions() gets called whenever .get_object() method is called in a generic view i.e. a particular resource is being accessed.
Now, all the detail requests (retrieve, update, delete) access a particular resource/object, it works. For create requests, get_object() method does not get called, so it does not work in your case.
For handling create requests, you can create another serializer where there is validations for the above 2 conditions. And then change your permission classes to work for detail requests only.
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."