Update instance for author in Django REST - django

I would like to update a instance of a model only if the request author is same than instance author.
I guess can do it in the update method:
def update(self, request, *args, **kwargs):
if request.user == self.get_object().user
do_things()
How can I do it? Is it obligatory to write an update en every ModelViewSet or ListAPIView? or is there a method to write a custom permission to accomplish this.

You can implement a custom permission. The following example is from the docs, modified to fit your use case:
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
# Instance must have an attribute named `user`.
return obj.user == request.user

Related

DRF - how to implement object based permission on queryset?

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

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 (drf) dynamic permissions from BasePermission

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.

What's the differences between has_object_permission and has_permission?

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.

Should I remove user id from URL if I want to keep data available just to exactly one user?

Lets just suppose that we have following url:
example.com/users/1
if user with ID=1 opens it, user receive info about his account, but if user switch 1 with 2, then he can view other user details as well, and we of course do not want that, so I have following solutions:
1) just pass currently logged in user id threw request.user.id inside a template
2) add permission, but I did not find type of permissions that would allow me to do that. Of course I could create dozens of permissions each for each user, but of course that is very nasty way.
Any other ideas how to cope with that in Django?
You can either fill context with the request which makes sure the user will never see another user's data, e.g. (using CBVs):
class AccountView(TemplateView):
"""
Generic account view
"""
template_name = "users/account.html"
def get_context_data(self, **kwargs):
context = super(AccountView, self).get_context_data(**kwargs)
context['user'] = User.objects.get(id=self.request.user.id)
return context
#method_decorator(login_required(login_url=reverse('login')))
def dispatch(self, *args, **kwargs):
return super(AccountView, self).dispatch(*args, **kwargs)
Another approach, to make sure 'fake' urls render 404's is to write an owner_required decorator, e.g.:
def owner_required(function):
#wraps(function)
def decorator(*args, **kwargs):
request = args[1]
user = get_object_or_404(User, username=request.user.username)
if user.is_authenticated() and user.username == kwargs.get('slug'):
return function(*args, **kwargs)
raise Http404
return decorator
You don't need permission to do this.
In your views.py
from django.http import Http404
def myview(request, user_id):
user = request.user
if user_id != request.user.id:
raise Http404
#all your logic here
But if you want user profile to be private, you don't need to use user_id in your url pattern. just use the user object stored in the request variable.