has_object_permission not being called at all in `(object)-detail` URLS - python

I have a problem in that has_object_permission() gets ignored. Even when I access http://127.0.0.1:8000/portfolios/3/ with the correct user logged in, it still defaults to has_permission(). Am I doing something wrong?
ViewSet class:
class PortfolioViewSet(viewsets.ModelViewSet):
queryset = Portfolio.objects.all()
serializer_class = serializers.PortfolioSerializer
permission_classes = (permissions.IsPortfolioOwner, )
Permission Class:
class IsPortfolioOwner(permissions.BasePermission):
# Details
def has_object_permission(self, request, view, obj):
print("Checking for object")
ruser = request.user
if ruser is None:
return False
elif ruser == obj.client.user:
return True
def has_permission(self, request, view):
print("Checking for list")
return request.user.is_superuser

In order for has_object_permission to be checked, has_permission must return True. If it returns False, then permission checks will short-circuit and the request will be denied.
Your current permission class will only allow the user to view the list if they are a superuser. And an individual object cannot be viewed under they are a superuser and viewing the current user's object.

Related

How to use has_object_permission to check if a user can access an object in function based views

I have a note object which can be accessd from notes/{pk}. If the method is GET or a read only method I was to allow anyone access to the note as long as the note is public (note.is_private = False)
I've implemented this as:
#api_view(['GET', 'DELETE', 'PUT'])
def detail_notes(request, pk):
try:
note = Note.objects.get(pk=pk)
except Note.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
response = NoteSerializer(note)
return Response(response.data)
...
If the method is PUT or DELETE I want to allow access to the note only if the current user is the owner of the note. I implemented this permission (according to the docs) as follows:
class IsOwnerOrIsPublic(BasePermission):
def has_object_permission(self, request, view, obj):
user = obj.user
privacy = obj.is_private
if request.method in SAFE_METHODS:
return not privacy # is the note public and is this a read only request ?
return request.user == obj.user
However, when I add the #permission_classes([IsOwnerOrIsPublic]) decorator to my view the permission doesn't restrict access to an unauthorized user. I'm able to view any note with a pk.
I tried explicitly calling IsOwnerOrIsPublic.has_object_permissions(), with this code in my view:
authorized = IsOwnerOrIsPublic.has_object_permission(request, note)
if not authorized:
return Response(status=HTTP_401_UNAUTHORIZED)
But I get the error has_object_permission() missing 2 required positional arguments: 'view' and 'obj' (obviously), and I do not know what other arguments to pass in. For example, what is the view argument?
How do I make this permission work on this view? Alternatively, how do I make this constraint work?
Note that my solution is based on generic views which is more simpler to implement.
Try the following view class:
from rest_framework import generics
from django.shortcuts import get_object_or_404
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
...
# generics.RetrieveUpdateDestroyAPIView will allow Get, put, delete
class NoteDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
"""
Retrieve, update, or delete note by its author
Retrieve only for readers
"""
# I'll assume you are using token authentication
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated, IsOwnerOrIsPublic,)
serializer_class = NoteSerializer
def get_object(self):
note = get_object_or_404(Note, pk=self.kwargs['pk'])
self.check_object_permissions(self.request, note)
return note

DRF and Token authentication with safe-deleted users?

I'm using a Django package named django-safedelete that allows to delete users without removing them from the database.
Basically, it adds a delete attribute to the model, and the queries like User.objects.all() won't return the deleted models.
You can still query all objects using a special manager. For example User.objects.all_with_deleted() will return all users , including the deleted ones. User.objects.deleted_only() will return the deleted ones.
This works as expected, except in one case.
I'm using Token Authentication for my users with Django Rest Framework 3.9, and in my DRF views, I'm using the built-in permission IsAuthenticated.
Code of a basic CBV I'm using:
class MyView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request):
return Response(status=HTTP_200_OK)
Code of the DRF implementation of IsAuthenticated permission:
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
The problem
When a user is soft deleted, he's still able to authenticate using its token.
I'm expecting the user to have a 401 Unauthorized error when he's soft deleted.
What's wrong?
The DRF already uses the is_active property to decide if the user is able to authenticate. Whenever you delete a user, just be sure to set is_active to False at the same time.
For django-safedelete:
Since you're using django-safedelete, you'll have to override the delete() method to de-activate and then use super() to do the original behavior, something like:
class MyUserModel(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE
my_field = models.TextField()
def delete(self, *args, **kwargs):
self.is_active = False
super().delete(*args, **kwargs)
def undelete(self, *args, **kwargs):
self.is_active = True
super().undelete(*args, **kwargs)
Note that this works with QuerySets too because the manager for SafeDeleteModel overrides the QuerySet delete() method. (See: https://github.com/makinacorpus/django-safedelete/blob/master/safedelete/queryset.py)
The benefit to this solution is that you do not have to change the auth class on every APIView, and any apps that rely on the is_active property of the User model will behave sanely. Plus, if you don't do this then you'll have deleted objects that are also active, so that doesn't make much sense.
Why?
If we look into the authenticate_credentials() method of DRF TokenAuthentication [source-code], we could see that,
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
Which indicates that it's not filtering out the soft deleted User instances
Solution?
Create a Custom Authentication class and wire-up in the corresponding view
# authentication.py
from rest_framework.authentication import TokenAuthentication, exceptions, _
class CustomTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active or not token.user.deleted: # Here I added something new !!
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
and wire-up in views
# views.py
from rest_framework.views import APIView
class MyView(APIView):
authentication_classes = (CustomTokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request):
return Response(status=HTTP_200_OK)

DRF get_permissions doesn't appear to work correctly

I'm trying to configure permissions for my custom User model and I'm using a Viewset to run CRUD operations for the model. Depending on the operation, I need to set different permission requirements such that:
anyone can create a new User instance
only owners or admins can retrieve or update users
For some reason, when I define the get_permissions method inside my class, like so:
def get_permissions(self):
if self.action == 'create':
return [AllowAny(),]
elif self.action == 'me':
return [IsAuthenticated(),]
return [IsAuthenticated(), IsOwnerOrAdmin()]
but when I navigate to /api/v1/users/, the DRF explorer doesn't show the form to create a new User and gives me a 403 error. On the flipside, when I comment this function out, I can see form properly, but obviously can't use it that way.
For reference, my custom IsOwnerOrAdmin permissions class looks like:
class IsOwnerOrAdmin(permissions.BasePermission):
#staticmethod
def _is_admin(request):
return request.user.is_superuser
def has_permission(self, request, view):
try:
is_owner = str(request.user.id) == view.kwargs.get('pk')
return is_owner or self._is_admin(request)
# if request.user.uuid is not there (i.e. AnonymousUser)
except AttributeError:
return False
What might be the issue here?
class IsOwnerOrAdmin(BasePermission):
#staticmethod
def _is_admin(request):
return request.user.is_superuser
def has_object_permission(self, request, view, obj):
return obj == request.user or self._is_admin(request)

How to return a 403 in a ViewSet instead of 404

I'm working on an API and have this ViewSet:
class ProjectViewSet(viewsets.ModelViewSet):
# API endpoint that allows projects to be viewed or edited.
queryset = Project.objects.all()
serializer_class = ProjectSerializer
authentication_classes = used_authentication_classes
permission_classes = (IsOwner,)
#detail_route(methods=['get'])
def functions(self, request, pk=None):
project = self.get_object()
if project is None:
return Response({'detail': 'Missing project id'}, status=404)
return Response([FunctionSerializer(x).data for x in Function.objects.filter(project=project)])
A permission system is attached to this API. The permissions work fine for a single resource. But when I call api/projects which should return all of the projects the user has access to, it does in fact return all of the projects, regardless whether the user should be able to GET a certain project in the list or not.
So I overwrote the get_queryset method to only return the projects the user has access to:
def get_queryset(self):
if self.request.user.is_superuser or self.request.user.is_staff:
return Project.objects.all()
else:
return Project.objects.filter(user=self.request.user.user)
This works, but now the API returns a 404 instead of a 403 when I ask for a specific resource I don't have access to.
I understand why, because the PK from the resource I try to get is undefined since I only return projects the user has access to. What I don't understand is how to fix this.
Does anyone know how I can make it return a 403, or maybe an idea towards where I should look?
as #Alasdair say the 404 is a deliberate choice, but if you still want to get 403 you can try:
def get_queryset(self):
user = self.request.user
allow_all = user.is_superuser or user.is_staff
if self.action == 'list' and not allow_all:
return Project.objects.filter(user=user)
return Project.objects.all()

django rest framework viewset permission based on method

So I'm writing my first project with DRF and I'm having some issues with setting up permissions for my viewsets. I already have authentication working with djangorestframework-jwt. Currently, I have a few different ViewSets defined. What I would like to do is allow the owner of a model object to make any changes they would like to that object, but prevent everyone else (aside admins) from even viewing the objects. Basically, I need a way of applying permission classes to specific methods to allow only admins to view 'list', owners to 'update, destroy, etc' and authenticated users to 'create'. Currently I have something like this:
class LinkViewSet(viewsets.ModelViewSet):
queryset = Link.objects.all()
serializer_class = LinkSerializer
with a model of
class Link(models.Model):
name = models.CharField(max_length=200)
url = models.URLField()
# another model with a OneToMany relationship
section = models.ForeignKey('homepage.LinkSection', related_name='links', on_delete=models.CASCADE
owner = models.ForeignKey('homepage.UserProfile'), related_name='links', on_delete=models.CASCADE)
and the permissions class I want to apply
class IsOwner(permissions.BasePermission):
def has_object_permissions(self, request, view, obj):
return obj.owner == request.user.userprofile
I'm sure it's possible to achieve this by writing completely custom views but I have a gut feeling that there is an easier way to do this especially since this is basically the last thing I have to do to finish the API. Thanks for any help and let me know if you need any more info.
I was able to create a permission class by checking which action was used in the view as follows here:
class IsOwner(permissions.BasePermission):
'''
Custom permission to only give the owner of the object access
'''
message = 'You must be the owner of this object'
def has_permission(self, request, view):
if view.action == 'list' and not request.user.is_staff:
print('has_permission false')
return False
else:
print('has_permission true')
return True
def has_object_permission(self, request, view, obj):
print('enter has_object_permission')
# only allow the owner to make changes
user = self.get_user_for_obj(obj)
print(f'user: {user.username}')
if request.user.is_staff:
print('has_object_permission true: staff')
return True
elif view.action == 'create':
print('has_object_permission true: create')
return True
elif user == request.user:
print('has_object_permission true: owner')
return True # in practice, an editor will have a profile
else:
print('has_object_permission false')
return False
def get_user_for_obj(self, obj):
model = type(obj)
if model is models.UserProfile:
return obj.user
else:
return obj.owner.user
get_user_for_obj is specifically for my implementation as a helper method since my model is inconsistent in how to obtain a user instance. You don't want to make has_permission too restrictive because has_object_permission will only run if has_permission returns True or if the method is not overridden.

Categories