I have created a django rest custom permision and for testing purposes I made sure has_object_permission and has_permission retrun False.
from rest_framework import permissions
class IsWorker(permissions.BasePermission):
message = "Login with Supplier Account"
def has_object_permission(self, request, view, obj):
return False
def has_permission(self, request, view):
return False
How comes this view doesn't work as expected?
from packages.models import Package
from .serializers import WorkerPackageSerializer
from rest_framework.permissions import IsAuthenticated
from api.permissions import IsWorker
class WorkerPackageDetailViewSet(generics.RetrieveUpdateDestroyAPIView):
serializer_class = WorkerPackageSerializer
permission_classes = (IsAuthenticated, IsWorker)
lookup_field = "id"
def get_queryset(self):
return Package.objects.all()
Expectations:
I thought it would return a forbidden error with the message Login with Supplier Account but if the user is not authenticated it returns a not authenticated error and if the user is authenticated it returns the data without any error.
Related
I was using Django users model for my Django rest framework. For this I used Django's ModelViewSet for my User class.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
Serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'password']
extra_kwargs = {
'password' : {
'write_only':True,
'required': True
}
}
def create(self, validated_data):
user = User.objects.create_user(**validated_data)
Token.objects.create(user=user) # create token for the user
return user
But currently from postman when I make the request using the token of one user to view, delete, edit other users
http://127.0.0.1:8000/api/users/4/
Its able to edit/delete/view other users. I don't want that to happen and one user can make request on itself only is all I want.
This is my apps urls.py
urls.py
from django.urls import path, include
from .views import ArticleViewSet, UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='articles')
router.register('users', UserViewSet, basename = 'users')
urlpatterns = [
path('api/', include(router.urls)),
]
How can I prevent one user from accessing other users when they make GET/POST/PUT/DELETE request.
EDIT 1: After adding the IsOwnerOfObject class as provided in he answers below, now when I am requesting the detail of the user himself, I am getting
Authentication credentials were not provided.
Building from Ene's answer, authentication and permission classes needs to be provided.
Create a file named permissions.py.
from rest_framework import permissions
class IsOwnerOfObject(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user
next add the permission and authentication class to ModelViewSet:
from api.permissions import IsOwnerOfObject
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated, IsOwnerOfObject]
authentication_classes = (TokenAuthentication,)
Create a file named permissions.py.
from rest_framework import permissions
class IsOwnerOfObject(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user
next add the permission class to you ModelViewSet:
from yourapp.permissions import IsOwnerOfObject
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsOwnerOfObject, <other permission classes you want to use>]
More info here:
https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#object-level-permissions
If you want to disable delete completely (Which is probably correct since if you want to "delete" a User you should deactivate it instead.) Then you can replace your view with this:
from rest_framework import viewsets
from rest_framework import generics
class UserViewSet(
generics.CreateModelMixin,
generics.ListModelMixin,
generics.RetrieveModelMixin,
generics.UpdateModelMixin,
generics.viewsets.GenericViewSet
):
queryset = User.objects.all()
serializer_class = UserSerializer
And then you can use Ene Paul's answer to limit who can edit.
Another all-in-one solution could be to use a queryset filter to directly narrow the queryset results. This will prevent an user to delete other users, but also prevent any unauthorized retrieving or listing as the only accessible data will be the user itself only.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
"""
The get_queryset function is used to get the queryset of the user data corresponding to the logged in user.
It is called when the view is instantiated, and it returns a list containing this user only.
"""
# after get all products on DB it will be filtered by its owner and return the queryset
owner_queryset = self.queryset.filter(username=self.request.user.username)
return owner_queryset
And this can also be used with other objects to allow retrieving only data related to this user.
i am using Django rest framework for my login api. Now i want to login only certain group user through api. I have created group and assigned user to the group.I have created a permission class and used to APIView but it's show ""Authentication credentials were not provided."
Here is my permissions.py class
from rest_framework.permissions import BasePermission
class FanOnlyPermission(BasePermission):
def has_permission(self, request, view):
if request.user and request.user.groups.filter(name='fan'):
return True
return False
Here is my view.py
from rest_framework import views, permissions, status, generics
class UserLoginApiView(generics.GenericAPIView):
"""
User Login Api View
"""
permission_classes = (FanOnlyPermission, )
def post(self, request):
"""
Handle POST request
:param request:
:return:
"""
serializer = UserLoginSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I have tried through postman. when i print request.user from permission.py it shoed "Anonymous user".
You need to implement custom authentication backend,
See this answer: how to create login authentication based on some condition in django restframework
And then config the AUTHENTICATION_BACKENDS in the settings.py:
AUTHENTICATION_BACKENDS = ['accounts.backends.AuthBackend']
I have a DRF view-set with a custom permission and filter. In DRF's official docs it says:
Permission checks are always run at the very start of the view, before any other code is allowed to proceed.
But I have noticed that my filter_backend class is called before permission_class. Here is my code:
# my permission
from rest_framework import permissions
class CompanyAccessPermission(permissions.BasePermission):
message = 'Detail of company not allowed.'
def has_object_permission(self, request, view, obj):
print("permission")
return request.user in obj.users.all()
# my filter
from rest_framework import filters
class IsCompanyOwnerFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
print("filter")
return queryset.filter(users__in=[request.user])
# my view
from rest_framework import mixins, viewsets
from rest_framework.permissions import IsAuthenticated
from api import filters, permissions, serializers
from core import models
class CompanyViewSet(viewsets.GenericViewSet,
mixins.ListModelMixin,
mixins.RetrieveModelMixin):
permission_classes = (IsAuthenticated, permissions.CompanyAccessPermission)
filter_backends = [filters.IsCompanyOwnerFilterBackend]
queryset = models.Company.objects.all()
serializer_class = serializers.CompanySerializer
So when I want to retrieve a Company object the output is as follows:
> filter
> permission
I was expecting the opposite of that. I also looked at the source code of DRF class GenericViewSet(ViewSetMixin, generics.GenericAPIView). It seems like the permission class (called in views.APIView) is called before the filter backend class (called in generics.GenericAPIViewi which inherits views.APIView). What am I missing?
Okay I have noticed what's going on. Here is the execution of permission_class methods and filter_backend_methods:
permission_classes' .has_permission(self, request, view) method, which I did not override.
filter_backends' .filter_queryset(self, request, queryset, view) method.
permission_classes' .has_object_permission(self, request, view, obj) method, which I did override.
As I was performing an object-level permission (overridinghas_object_permission method), my custom filter was executed before, which makes more sense.
I build a very basic ViewSet on User model to CRUD a user.
I want to use ModelViewSet and #action decorator to make codes clean.
Set a permission(Just use IsAuthenticated as example) required on the list function. so that only who are signed can do this.
This is the code sample.
from rest_framework.decorators import action, list_route
from rest_framework.permissions import IsAuthenticated
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
#action(detail=False, permission_classes=[IsAuthenticated])
def list(self, request, *args, **kwargs):
return super(UserViewSet, self).list(request, *args, **kwargs)
But I got an error
Cannot use the #action decorator on the following methods, as they are existing routes: list
If I remove #action, it works well. My question is why I cannot use #action decorator on an existing routes list?
#action is for "extra actions" which actually means (if you inspect the code): "not explicityly defined in the router". So if you are registering your view in urls using DefaultRouter or SimpleRouter, then router will throw this error.
For your case you can modify View.get_permissions as shown in docs example:
from rest_framework.permissions import IsAuthenticated
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_permissions(self):
if self.action == 'list':
return [IsAuthenticated()]
else:
return super(self, UserViewSet).get_permissions()
Even if it doesn't answer the specific question of this user, I'm gonna post for anyone arriving here after the misleading title, you can use #action decorators for a ModelViewSet
Define the viewset as following:
from rest_framework import viewsets
class MyModelViewSet(viewsets.ModelViewSet):
serializer_class = MyModelSerializer
queryset = MyModel.objects.all()
#action(detail=True, methods=['get'], url_path='my-action')
def my_action(self, request, *args, **kwargs):
""" Get last property-run and return associated results """
instance = self.get_object()
# custom code
custom_response = {"test": "hello world"}
return Response(custom_response, status=status.HTTP_200_OK)
Define the router and apis
from rest_framework import routers
from django.conf.urls import url
router = routers.SimpleRouter()
router.register(r'mymodel', MyModelViewSet)
urlpatterns += [
url(r'^api/1/', include(router.urls))
]
Then simply access it via api/1/mymodel/1/my-action
You have not imported action
from rest_framework.decorators import action
See the examples on drf documentation
As per the DRF documentation, the serializer_class attribute should be set when using GenericAPIView. But why does the serializer_class attribute even works with APIView?
Here is my API code:
class UserView(APIView):
serializer_class = SignupSerializer
#transaction.atomic
def post(self, request):
email = get_data_param(request, 'email', None)
password = get_data_param(request, 'password', None)
params = request.POST.copy()
params["username"] = email
serializer = UserSerializer(data=params)
if serializer.is_valid(raise_exception=True):
user = serializer.save()
user.set_password(password)
user.save()
params["user"] = user.id
serializer = CustomProfileSerializer(data=params)
if serializer.is_valid(raise_exception=True):
profile = serializer.save()
return Response(response_json(True, profile.to_dict(), None))
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField(max_length=100)
password = serializers.CharField(max_length=50)
When I browse this API in the browser it does show the email and password fields as input but if I don't set this serializer_class attribute, no input fields are shown.
Ideally, this serializer_class attribute should not work with APIView. I have searched a lot but there is nothing available related to this.
Can anyone please provide an explanation for this behavior? Thanks.
I think this can help you.
create serializer.py and write:
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('email', 'password')
and views.py:
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer
from .models import User
class AddUserAPIView(APIView):
def post(self, request):
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user = user_serializer.save()
user.set_password(user_serializer.data["password"])
return Response({'message': 'user added successfully!'})
return Response({'message': user_serializer.errors})
You are absolutely right!!:
APIView doesn't utilize a serializer_class (by default) because it is not meant to handle any request processing logic!
What happens though is that the BrowsableAPIRenderer that is used to render the API in the browser checks for a serializer_class attribute and set's it as the View serializer if it exists. We can see this in the BrowsableAPIRenderer code:
The _get_serializer class of the renderer:
def _get_serializer(self, serializer_class, view_instance, request, *args, **kwargs):
kwargs['context'] = {
'request': request,
'format': self.format,
'view': view_instance
}
return serializer_class(*args, **kwargs)
And the way it is used to set the renderer serializer if it exists, inside the get_rendered_html_form:
Line 483: has_serializer_class = getattr(view, 'serializer_class', None)
Lines 497 to 509:
if has_serializer:
if method in ('PUT', 'PATCH'):
serializer = view.get_serializer(instance=instance, **kwargs)
else:
serializer = view.get_serializer(**kwargs)
else:
# at this point we must have a serializer_class
if method in ('PUT', 'PATCH'):
serializer = self._get_serializer(view.serializer_class, view,
request, instance=instance, **kwargs)
else:
serializer = self._get_serializer(view.serializer_class, view,
request, **kwargs)
In essence, you accidentally override the BrowserAPIRenderer's default behavior regarding the APIView by providing the serializer_class attribute. For what is worth, my opinion on the matter is that this should not be possible!
I use the django rest framework default get_schema_view() to provide auto-generated openapi schema from which I auto generate a javascript client for.
This works for ViewSets, but the payload wasn't being provided for views defined by APIView.
Where I have defined serializers, I found that adding get_serializer() method to my APIView classes allowed the schema to be generated with the serializer defined payload.
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer
from .models import User
class AddUserAPIView(APIView):
def get_serializer(self, *args, **kwargs):
return UserSerializer(*args, **kwargs)
def post(self, request):
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user = user_serializer.save()
user.set_password(user_serializer.data["password"])
return Response({'message': 'user added successfully!'})
return Response({'message': user_serializer.errors})