I am trying to enforce a custom permission in DRF. Specifically, I am trying to see whether a user is authorized to access an object. I adapted this solution from another post here but it does not quite work for me. The permission class always assumes that request.user is an Anonymous user. What am I missing?
permissions.py
class CanSeeWishlist(permissions.BasePermission):
def has_permission(self, request, view):
try:
wishlist = Wishlist.objects.get(
pk=view.kwargs['pk'])
except:
return False
if wishlist.private:
print(request.user) # Prints Anonymous User
if request.user.id == wishlist.owner.id or request.user.id in wishlist.members.all():
return True
return False
return True
api.py
class WishlistViewset(viewsets.ModelViewSet):
serializer_class = WishlistSerializer
queryset = Wishlist.objects.all()
authentication_classes = (TokenAuthentication,)
permission_classes = [
permissions.IsAuthenticatedOrReadOnly
]
def get_permissions(self):
if self.request.method == 'GET':
self.permission_classes = (CanSeeWishlist,)
return super(WishlistViewset, self).get_permissions()
I suspect that doing super(WishlistViewset, self).get_permissions() didn't pass request to the class so it resulted in Anonymous User or as you dropped IsAuthenticatedOrReadOnly something went wrong.
Anyway, I think it's better to override get_permissions() with the same way it was implemented, it will fix the problem I hope:
What get_permissions() actually do:
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
return [permission() for permission in self.permission_classes]
So It should be:
def get_permissions(self):
permissions_list = [permissions.IsAuthenticatedOrReadOnly] #the base one that should be there for all request actions
if self.request.method == 'GET': # this will add the permission for all GET requests, If you want it to only apply on retrive you can do self.action == 'retrive':
permissions_list.append(CanSeeWishlist)
return [permission() for permission in permissions_list]
Related
Why is my action decorator ignoring its permission_class?
I have a ViewSet that has "IsAuthenticated" for both a generic post/create and for posting to a custom action decorator.
However, when I use the custom action decorator for a non-logged in user, the code for the action decorator still runs (and causes an error).
Why is this? Shouldn't a non-logged in user receive a 401_Unauthorized when posting to the action?
A non-logged in user does receive a 401_Unauthorized when doing a generic post.
Here is the ViewSet:
class ItemViewSet(viewsets.ModelViewSet):
queryset = Item.objects.all()
lookup_field = "pk"
serializer_class = ItemSerializer
def get_permissions(self):
if self.action == "create":
permission_classes = [IsAuthenticated]
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
#action(
methods=["post"],
detail=True,
permission_classes=[IsAuthenticated],
url_name="actioned",
)
def actioned(self, request, pk=None):
try:
item = Item.objects.get(user=request.user)
item.status = "actioned"
item.save()
return Response(status=status.HTTP_200_OK)
except Item.DoesNotExist:
Item.objects.create(user=request.user, status="actioned")
return Response(status=status.HTTP_201_CREATED)
I'm using DefaultRouter() for my urls:
router = DefaultRouter
router.register(r"items", ItemViewSet, basename="item")
urlpatterns = [ path("", include(router.urls)), ]
Here are the tests:
# GENERIC POST BY ANONYMOUSUSER DOESN'T RUN AND GIVES 401
def test_public_generic_post(self):
payload = {"status": None}
response = APIClient.post(
reverse("app:item-list"),
payload,
format="json"
)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) # True
# ACTION POST BY ANONYMOUSUSER RUNS EVEN THOUGH IT HAS THE SAME IsAuthenticated permission
def test_public_action_post(self):
item = Item.objects.create()
response = APIClient.post(
reverse("app:item-actioned", args=[item.pk]),
format="json"
)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) # False
For the action post, I receive the following error:
TypeError: Cannot cast AnonymousUser to int. Are you trying to use it in place of User?
Because it's running my try: item = Item.objects.get(user=request.user) code and there is no request.user.
But why would it run the action decorator in the first place if the action decorator explicitly has permission_classes=[IsAuthenticated]?
I can confirm that request.user.is_authenticated is False.
As per #KutayAslan's comment, removing the get_permissions method resolves this error.
It looks like in my original code, the action decorators were being assigning [AllowAny] due to its "self.action" falling under the "else" clause.
Setting up the permission_classes to explicitly equal the action decorator resolves this issue:
def get_permissions(self):
if self.action == "create":
permission_classes = [IsAuthenticated]
elif self.action == "actioned":
permission_classes = [IsAuthenticated]
else:
permission_classes = [AllowAny]
return [permission() for permission in permission_classes]
Another possibility for anyone looking at this thread is that you mis-spelled the BasePermission functions as has_permissions and has_object_permissions (plural) instead of has_permission and has_object_permission (singular)!
I was just watching some online tutorial on how to use Django Rest Framework to create a basic REST API using their default router. Link to Docs
but then because he used a model viewset he had to add permission_classes to them which means all different types of requests whether its post or get or others it'll all take the same permission.
I was wondering if there's a way to give them different permission_classes depending on the type of request.
Yes, you can write your own permissions.
Just create some python file. I named it permissions.py:
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import BasePermission
from company.models import Company
class IsGetOrIsAuthenticated(BasePermission):
def has_permission(self, request, view):
if request.method == 'GET':
return True
return request.user and request.user.is_authenticated
class IsGetOrIsCompanyOwner(BasePermission):
def has_permission(self, request, view):
if request.method == 'GET' or 'OPTIONS':
return True
elif request.method == 'DELETE':
company = get_object_or_404(Company, id=view.kwargs['pk'])
return request.user.user_type == 'moder' or company.owner == request.user
else:
company = get_object_or_404(Company, id=view.kwargs['pk'])
return company.owner == request.user or request.user.user_type == 'moder'
class IsModer(BasePermission):
def has_permission(self, request, view):
return request.user.user_type == 'moder'
After that you can use them in views.py
from company.permissions import IsGetOrIsAuthenticated, IsGetOrIsCompanyOwner, IsModer
class ActiveCompanyShortView(ModelViewSet):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsGetOrIsAuthenticated,)
you can read more there
To achieve what you need, one possible solution is to override the get_permissions() of ViewSet.
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
if self.action == 'list':
return [objects of permissions_u_need_in_list_view]
elif self.action == 'create':
return [objects of permissions_u_need_in_create_view]
This is what DRF provides the definition of get_permissions().
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
return [permission() for permission in self.permission_classes]
I want to add different permissions for different methods of ModelViewset class using decorator.
I tried:
class abcd(viewsets.ModelViewSet):
#permission_classes(IsAuthenticated,))
def list(self, request, format=None):
try:
#permission_classes(CustomPermission,))
def create(self, request, format=None):
try:
But it isn't working.
I also tried using #method_decorator. That didn't worked either.
I know we can do in the following way:
def get_permissions(self):
if self.action == 'create':
return [IsAuthenticated(), ]
return super(abcd, self).get_permissions()
But I was wondering if we can achieve this using decorators for Django Rest Framework.
ModelViewSet inherits Mixin classes and GenericAPIView. The methods list and create are from Mixins hence decorating with permission_classes won't work. Instead, you can try overriding get_permissions in APIView.
def get_permissions(self):
if self.request.method == "GET":
return [IsAuthenticated()]
elif self.request.method == "POST":
return [CustomPermission()]
return [permission() for permission in self.permission_classes]
Note: I am not sure whether above code works or not
a sample code is shown in official page.
so it might be better not using self.request.method but self.aciton property.
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
if self.action == 'list':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdmin]
return [permission() for permission in permission_classes]
https://www.django-rest-framework.org/api-guide/viewsets/#introspecting-viewset-actions
I have a basic Viewset:
class UsersViewSet(viewsets.ModelViewSet):
permission_classes = (OnlyStaff,)
queryset = User.objects.all()
serializer_class = UserSerializer
It is bind to the /api/users/ endpoint. I want to create a user profile page, so I need only a particular user, so I can retrieve it from /api/users/<id>/, but the problem is that I want /api/users/<id>/ to be allowed to anyone, but /api/users/ to keep its permission OnlyStaff, so no one can have access to the full list of users.
Note: Perhaps it's not such a good implementation, since anyone could brute force the data incremeting the id, but I'm willing to change it from <id> to <slug>.
How can I delete the permission from detail route?
Thanks in advance.
Override the get_permissions() method as below
from rest_framework.permissions import AllowAny
class UsersViewSet(viewsets.ModelViewSet):
permission_classes = (OnlyStaff,)
queryset = User.objects.all()
serializer_class = UserSerializer
def get_permissions(self):
if self.action == 'retrieve':
return [AllowAny(), ]
return super(UsersViewSet, self).get_permissions()
It would help if you posted the permission class.
But going off what you posted, it appears that only staff users can have access to the endpoints bound to that viewset. Meaning no other user type/role can access those endpoints.
Going off your question, it seems like you want to setup a IsOwnerOrStaffOrReadOnly permission and over ride the list route function of the ModelViewSet and replace permission_classes and then call super
class UsersViewSet(viewsets.ModelViewSet):
permission_classes = (IsOwnerOrStaffOrReadOnly,)
queryset = User.objects.all()
serializer_class = UserSerializer
def list(self, request, *arg, **kwargs):
self.permission_classes = (OnlyStaffCanReadList,)
super(UsersViewSet, self).list(request, *args, **kwargs) // python3 super().list(request, *args, **kwargs)
is Owner object permission class
class IsOwnerOrStaffOrReadOnly(permissions.BasePermission):
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
if request.user.role == 'staff':
return True
# Instance must have an attribute named `owner`.
return obj.owner == request.user
only staff can read permission class
class OnlyStaffCanReadList(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.user.role == 'Staff':
return True
else:
return False
as provided in the comments, your user model must have the owner role. if you are using the django user model you can just do a obj.id == request.user.id comparison
How to add custom permission in viewset in django rest framework other than the default permission while creating a module?
I have a permission "fix_an_appointment". In the below viewset, how to include this permission? Those who have this permission has only able to create.
My views.py file:
class settingsViewSet(viewsets.ModelViewSet):
serializer_class = SettingsSerializer
queryset = Setting.objects.all()
Can anyone help?
I can't use a decorator like: #permission_classes(IsAuthenticated, ) in extra actions within ViewSet
To use different permissions in actions, instead, put it into the #action() as a parameter.
#action(detail=True, methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
drf doc
simply create a custom permission class
class FixAnAppointmentPermssion(permissions.BasePermission):
def has_permission(self, request, view):
return True or False
then the in your view set class use your custom permission
class settingsViewSet(viewsets.ModelViewSet):
serializer_class = SettingsSerializer
queryset = Setting.objects.all()
permission_classes = (FixAnAppointmentPermssion,)
by docs custom-permissions, list of view actions actions
my_permissions.py
from rest_framework import permissions
class FixPermission(permissions.BasePermission):
"""
fix_an_appointment
"""
def has_permission(self, request, view):
if request.user.is_authenticated :
if view.action == 'retrieve':
return request.user.has_perms('fix_list_perm')
if view.action == 'retrieve':
return request.user.has_perms('fix_an_appointment')
return False
in views.py
from my_permissions import FixPermission
class settingsViewSet(viewsets.ModelViewSet):
serializer_class = SettingsSerializer
queryset = Setting.objects.all()
permission_classes = (FixPermission,)
We can set permission for each functions like create, retrive, update, delete(add,edit,delete and update)
from my_permissions import FixPermission
class FixAnAppointmentPermssion(permissions.BasePermission):
def has_permission(self, request, view):
return True or False
class YourViewSet(viewsets.ModelViewSet):
serializer_class = SettingsSerializer
queryset = Your.objects.all()
#permission_classes(FixAnAppointmentPermssion,)
def create(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
#permission_classes(FixAnAppointmentPermssion,)
def retrive(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)