class StoryViewSet(viewsets.ModelViewSet):
serializer_class = StorySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
I have viewset with all CRUD functions. The problem is that user can edit or delete stories of another user, I found DjangoModelPermissionsOrAnonReadOnly in permissions, but I cannot even create. I am using author as foreign key in Storymodel. Also I am using rest_framework.authtoken. So, I think there is two options, create own permission or rewrite some permission. Which one is better?
Write a customer object-level permission. Here is an example:
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
"""
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.author == request.user
And include it to permission_classes list.
Related
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
I'm trying to make a custom permission using this guide
views.py
class CustomModelList(generics.ListAPIView):
queryset = CustomModel.objects.all()
serializer_class = CustomModelSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsCustomOrReadOnly]
def get(self, request, format=None):
# some logic
def post(self, request, format=None):
# some logic
Just for experiment I've created this permission not to apply anyway
pesmissions.py
class IsCustomOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
return False
But when POST request sends to server it takes no effect -- I'm able to create new model instance.
I think that since you are using a list view, custom object level permissions are not checked automatically.
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.
You can try overriding the has_permission method instead and see if that works, or check the permissions manually.
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)
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.
Which is the proper way to test object based permissions?
Sample:
from rest_framework import permissions
class IsOfficeAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
office = obj
return office.admin == request.user
Which are the "asserts" that I shouldn't miss?
Do I need to create a view?
To your questions:
it's up to you to write the logic which will allow a user to access the object. As a result, you have to return a Boolean.
yes. You will specify to the view which permission classes you want to apply. In case of object permissions they will be queried on detail routes (get, update, delete)