A snippet out of Django Rest Framework:
class IsAuthenticated(BasePermission):
def has_permission(self, request, view):
return request.user and is_authenticated(request.user)
def is_authenticated(user):
if django.VERSION < (1, 10):
return user.is_authenticated()
return user.is_authenticated
Is there a practical and relevant case where my own code would return unexpected or different results from the above?
class IsAuthenticated(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated
If request.user isn't defined yours will error. In other words if the user isn't identified so hasn't been added to the request object.
If you're not interested in backwards compatibility I suppose you could do:
return request.user and request.user.is_authenticated
Related
This is a snippet from the Django rest framework documentation on writing custom permissions. I don't understand the meaning of the last line here:
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
The method has_object_permission() returns True or False depending in the evaluation of obj.owner == request.user
I don't seem to be triggering the has_object_permission function.
class MyPermission(DefaultPermission):
def has_permission(self, request, view):
return request.user.is_superuser
def has_object_permission(self, request, view, obj):
import pdb;pdb.set_trace()
return request.user == obj.submitter
Going to mydomain.com/api/mymodel/100, this should be considered accessing the object, correct? I am viewing object 100 here. Why isn't my trace() being picked up?
View
class MyModelViewSet(viewsets.ModelViewSet):
serializer_class = MyModelSerializer
permission_classes = (MyPermission,)
def get_queryset(self):
queryset = MyModel.objects.all()
return queryset
In a DRF generic views check_permissions(request) is always called in every request since it is inside the dispatch method.
check_permissions method collects all the permissions from the permission classes and checks each permission. If any permission return false the method raises an exception.
So if your has_permission method returns false has_object_permission is not called.
check_object_permissions(request, obj) calls has_object_permission method on each permission class. It is called inside get_object method.
So the rule is if has_permission for all permission classes returns true then and only then has_object_permission is checked.
class MyPermission(DefaultPermission):
def has_permission(self, request, view):
# check if the request is for a single object
if view.lookup_url_kwarg in view.kwargs:
return True
return request.user.is_superuser
def has_object_permission(self, request, view, obj):
import pdb;pdb.set_trace()
return request.user == obj.submitter
Permission won't work because check_object_permissions method is called only in get_object function. So you should call that function in your views:
def check(self, request, pk=None):
obj = self.get_object()
Or you could add permissions directly in the decorator,
Example
class MyViewSet(ModelViewSet):
queryset = MyModel.objects.all()
....
permission_classes = (MyPermission, )
#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')
From the docs(django-rest-framework)
Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already passed. Also note that in order for the instance-level checks to run, the view code should explicitly call .check_object_permissions(request, obj). If you are using the generic views then this will be handled for you by default.
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.
In django rest framework, whenever a permission is denied, it returns 401 to the client.
However this is very bad for items that are hidden. By sending 401 you acknowledge the user that infact, there is something there.
How can I instead return 404 in specific permissions? This one for example:
class IsVisibleOrSecretKey(permissions.BasePermission):
"""
Owner can view no matter what, everyone else must specify secret_key if private
"""
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
key = request.query_params.get('secret_key')
return (
obj.visibility != 'P'
or
request.user == obj.user
or
obj.secret_key == key
)
Going off of the DjangoObjectPermissions in the Django Rest Framework GitHub, you can raise an Http404. So instead of returning True or False, you return True if everything is good and then raise Http404 if not:
from django.http import Http404
class IsVisibleOrSecretKey(permissions.BasePermission):
"""
Owner can view no matter what, everyone else must specify secret_key if private
"""
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
key = request.query_params.get('secret_key')
if obj.visibility != 'P' or request.user == obj.user or obj.secret_key == key:
return True
else:
raise Http404
Since DjangoObjectPermissions extends BasePermission (a few steps back) this shouldn't do anything unexpected - I've just tested to make sure it returns 404 though, I haven't used it outside of this context. Just a head's up to do some testing.
I have a Django project that uses profiles for user information. Things are somewhat working except for one aspect... Here are code snippets to describe my problem.
In the template:
<li>Profile</li>
In views.py
class UserProfileView(View):
#method_decorator(login_required)
def get(self, request, user):
profile = get_object_or_404(UserProfile, user=request.user)
return render(request, 'accounts/profile.html', {'profile': profile})
In urls.py
url(r'^accounts/(?P<user>.+)/profile/$',
UserProfileView.as_view(),
name='user_profile_view'
),
I've tried variations for the named group, and this is what I found to work. The problem is, I can use any string in between /accounts/ and /profile/ (obviously) and it works. What I want to accomplish is to have only the current user's username be valid in the URL and otherwise throw a 404.
Do you really need the user parameter in the profile URL? If you only want it work for the current user, then why not simply drop the user parameter:
# urls.py
url(r'^accounts/profile/$',
UserProfileView.as_view(),
name='user_profile_view'
),
# views
class UserProfileView(View):
#method_decorator(login_required)
def get(self, request):
profile = get_object_or_404(UserProfile, user=request.user)
return render(request, 'accounts/profile.html', {'profile': profile})
In the code you posted, the UserProfileView.get method was not using the user parameter anyway.
UPDATE
If you want to keep the user parameter and make it work the way you want, you can change the view like this:
from django.http import Http404
class UserProfileView(View):
#method_decorator(login_required)
def get(self, request, user):
if request.user.username == user:
profile = get_object_or_404(UserProfile, user=request.user)
return render(request, 'accounts/profile.html', {'profile': profile})
else:
raise Http404
Btw, since user in the parameter list of the get method is really just the username as opposed to a user object, it would be better to rename it to username, to avoid confusion.