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."
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 have one model which has user as its ForeignKey attribute which is auto fill ie. logged in user is filled there. I have made token authentication. Only Authenticated // i mean authorized users can visit that view. But i am planning to make such that only the user which had created that model object can only update the content of that object.
For example:
class Something(models.Model):
sth_name = models.CharField(max_length=18)
sth_qty = models.IntegerField()
user = models.ForeignKey(User)
on my View:
I override perform_create() to associate to above model automaticall.
def perform_create(self, serializer):
return serializer.save(user=self.request.user)
What do i exactly need to do? I have to write some permissions method, But I am really stuck.
Yes, you need to create an object level permission. The DRF tutorial covers this nicely here: http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#object-level-permissions
Specifically, create a file permissions.py in your app, and add this permission there:
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.user == request.user
Then, in your view class which has the update resource for the Something model (probably SomethingDetail), add the permission_classes field:
class SomethingDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Something.objects.all()
serializer_class = SomethingSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
Just add the user when retrieving the object
obj = get_object_or_404(Something, pk=pk, user=request.user)
Note that this will throw 404. If you want 403 error, use custom condition to check the user and raise PermissionDenied. If you want to do this for multiple views, put the condition logic in a decorator.
I want to have easy way to check if somebody is owner or admin of post, proposal and so on he's trying to edit \ delete.
So, every time I use IsAuthenticated permission and in the method of ModelViewSet I get instance and check if instance.author or sometimes instance.owner is the user who requested it (request.user == instance.owner on some objects it's request.user == instance.author).
Question
The main question is: how can I create permission class that can check this kind of ownership with dynamic user attribute name on instance?
One of mine solutions (not the best, i think)
I've created function that take user attribute instance name returns permission class:
def is_owner_or_admin_permission_factory(owner_prop_name):
class IsOwnerOrAdmin(BasePermission):
def has_permission(self, request, view, *args, **kwargs):
instance = view.get_object()
try:
owner = getattr(instance, owner_prop_name)
except AttributeError:
return False
return (
request.user and request.user.id and (owner == request.user or request.user.is_staff)
)
return IsOwnerOrAdmin
I have also been frustrated over the same exact problem for days, and I've managed to find a suitable work-around (at least for me, of course), when dealing with multiple models with different lookup names for user attributes.
The work-around was something like this, in the ModelViewSet defined a separate attribute user_lookup_kwarg in the view, which could be used for checking the appropriate permissions.
Eg,
class YourViewSet(viewsets.ModelViewSet):
queryset = YourModel.objects.all()
serializer_class = YourSerializer
user_lookup_kwarg = 'user' #or 'account/created_by' whatever.
Now, your permission_class would be somewhat like this,
class CustomPermission(BasePermission):
def has_object_permission(self, request, view, obj):
try:
return request.user.is_superuser or getattr(obj, view.user_lookup_kwarg) == request.user
except:
return False
return request.user.is_superuser
You only just need to override has_object_permission() method to check instance level permissions.
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.