Require authorization for all but the listing view in a viewset - django-rest-framework

I have a ViewSet called BuildViewSet, with a bunch of detail views and one list view:
class BuildViewSet(viewsets.ModelViewSet):
queryset = Build.objects.all()
def list(self, request):
# Do some filtering on self.queryset based on user preferences
return super(BuildViewSet, self).list(request)
#detail_route(methods=['post'])
def transition(self, request):
…
# And a bunch of other methods, all prefixed with #detail_route
I set up REST Framework so the default authorization class is rest_framework.permissions.IsAuthenticated:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
However, I want my list() view to be available for everyone, even unauthenticated. I tried changing my list() method like this:
#list_route(permission_classes=(AllowAny,))
def list(self, request):
…
But this seems to have no effect:
AppError: Bad response: 401 UNAUTHORIZED (not 200 OK or 3xx redirect for http://localhost/api/v1/builds/)
'{"detail":"Authentication credentials were not provided."}
Changing the #detail_route to #permission_classes like this gives the same result:
#permission_classes((AllowAny,))
def list(self, request):
…
So it seems that list_route(…) is not the way to go, but in this case, what is?

You need to decorate the list method with the #permission_classes decorator, but the problem is that this decorator is only used for function-based views. Thus, you have two choices:
1) Convert the list view to a function-based view.
2) Authorize all views from the viewset by setting permission_classes = (AllowAny,) at the class level. In order to limit access to the other views, you will have to manually check the permissions using either a decorator or by calling the check_is_authenticated method:
def check_is_authenticated(self, request):
"""
Inspired from rest_framework.views.check_permissions
"""
if not IsAuthenticated.has_permission(request, self):
self.permission_denied(
request, message=getattr(permission, 'message', None)
)
Since all views that require a permission are already decorated with #detail_route, all you have to do is create a new #authenticated_detail_route decorator.
EDIT 3) Another, alternative solution would be to overload the check_permissions method:
def check_permissions(self, request):
if self.is_request_list(request):
return
return super(BuildViewSet, self).check_permissions(request)
The implementation of the is_request_list method is left as an exercise to the reader :-)
(seriously though, I'm not sufficiently familiar with django-rest-framework to offer an implementation. It would probably involve checking the request.method attribute)
EDIT As mentioned by the OP in a comment, in check_permissions the self.action attribute holds the "list" method name.

Related

Accessing ViewSet object list to provide extra context to serializer

I am attempting to add context to a serializer within a ModelViewSet which is dependent on the current paged object list in context. I'll explain with an example.
I am building a viewsets.ModelViewSet that lists Users and a list of favorite_foods. However- the list of user's favorite foods in some external microservice accessible via API. Building a ViewSet to collect objects and performing HTTP requests on each is trivial, we can do something like this:
class UserViewSet(viewsets.ViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserSerializer(serializers.ModelSerializer):
favorite_foods = serializers.SerializerMethodField()
def get_favorite_foods(self, instance):
# performs an HTTP call and returns a list[] of foods.
return evil_coupled_microservice_client.get_user_food_list(instance)
class Meta:
model = User
fields = ('id', 'name', 'favorite_foods')
The issue is (aside from some ugly infrastructure coupling) this is going to make an HTTP request count equivalent to the page size. Instead- it would be great if I could prefetch the favorite food lists for all users on the page in a single HTTP call, and just add them into context, like this:
class UserViewSet(viewsets.ViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
users = <-- This is where I want to know what users are in the current filtered, paginated response.
users_food_dict = evil_coupled_microservice_client.get_many_users_food_list(users)
context.update({'usesr_foods': users_food_dict})
return context
However- it doesn't appear there is any way to fetch the object list that's going to be serialized. Although (I'm fairly sure) get_serializer_context is called after the queryset is filtered and paginated, I'm not sure how to access it without doing some really hacking re-compiling of the queryset based on the query_params and other pieces attached to the class.
I'll post my current solution. It's not terrible, but I'm still hoping for a cleaner built-in.
class UserViewSet(viewsets.ViewSet):
...
def list(self, request, *args, **kwargs):
# Overrwite `ListModelMixin` and store current set
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
self.current_queryset = page
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
self.current_queryset = queryset
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
This is untested so far (not sure about functionality on Detail endpoints for instance) allows for the current_queryset to be fetched within the serializer context.

How to integrate/install drf-yasg to Django Rest project? Redoc/Swagger appears empty

I'm trying to integrate the drf-yasg to my Django Rest project. I installed the library via pip and added these code lines to the url.py as below.
schema_view = get_schema_view(
openapi.Info(
title="Costifier API",
default_version='v1',
description="Costifier API'ye hoşgeldiniz.",
terms_of_service="https://costifier.sfmyazilim.com",
contact=openapi.Contact(email="info#sfmyazilim.com"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('', index),
path('admin/', admin.site.urls),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), #<-- Here
path('api/', include('sfmAPI.urls')),
]
One of my views is;
class PredictionView(views.APIView):
permission_classes = [AllowAny]
throttle_classes = [AnonymousUserThrottle]
queryset = Prediction.objects.all()
serializer_class = PredictionSerializer
def post(self, request, format=None):
serializer = PredictionSerializer(data=request.data)
if serializer.is_valid():
input_map_dict = json.loads(serializer.validated_data['input_map'])
username = serializer.validated_data['customer_name']
prediction_results = SmartRegression.smart_predict(username,
serializer.validated_data['model_name'],
input_map_dict,
isMember(username))
result = {
'inputs': serializer.data,
'error': '0',
'message': 'Successful',
'predicted_value': prediction_results[0],
'confidence': prediction_results[1],
'feature_importance': prediction_results[2]
}
return Response(status=status.HTTP_200_OK, data=result)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
My /redoc page is created. However, it has no content. It just has the APIView names such as below.
How can I fill the documentation?
The reason is you're using APIView instead of a generic view.
In the Django-Rest-Framework docs for schemas it mentions the below:
Note: The automatic introspection of components, and many operation parameters relies on the relevant attributes and methods of GenericAPIView: get_serializer(), pagination_class, filter_backends, etc. For basic APIView subclasses, default introspection is essentially limited to the URL kwarg path parameters for this reason.
A work around for this if you want to stick to using APIView instead of a generic view, is to use #swagger_auto_schema decorator (from drf-yasg) above each of your views. It's a bit of a pain but you should be able to find a few post on stackoverflow around using this. An example is:
DRF YASG CustomizingDRF YASG Customizing
Django-Rest-Framework does have a manual method of setting the parameters but most people would prefer to stick to the AutoSchema doing the work. I haven't been able to find much on how to use it for a beginner either. Hence drf-yasg seems a good way to go.
I believe for your post view above the below decorator should be what you're looking for just above the post method:
#swagger_auto_schema(
request_body=PredictionSerializer,
responses={
'200': 'OK Request',
'400': "Bad Request"
},
)
Also you want to add something like the below to the very top of your view since it will pull through as the description in the schema/documentations.
'''
def post(self, request, format=None):
"Description of your View"
'''
...

How to Not allow the PUT method at all but allow PATCH in a DRF ViewSet?

PUT and PATCH are both part of the same mixin (The UpdateModelMixin).
So if I extend it like so:
class UserViewSet(mixins.UpdateModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
Both PUT and PATCH are allowed. I want to not allow PUT at all for my app (since PATCH already does the work, and I want to limit object creation using just POST). One way is to create a permission:
class NoPut(permissions.BasePermission):
"""
PUT not allowed.
"""
message = 'You do not have permission to complete the action you are trying to perform.'
def has_object_permission(self, request, view, obj):
if view.action == "update":
return False
return True
And to give this permission to all my ViewSets which allow PATCH. Is this the best way to do it? Is there a more preferred way?
Edit: After looking at the answer provided by #wim, will this be a fine solution (everything kept the same except the mapping for put was removed):
from rest_framework.routers import SimpleRouter
class NoPutRouter(SimpleRouter):
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes.
# Generated using #list_route decorator
# on methods of the viewset.
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
# put removed
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes.
# Generated using #detail_route decorator on methods of the viewset.
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
]
or would I need to redefine other methods in SimpleRoute (e.g. __init()__, get_routes(), _get_dynamic_routes(), get_method_map() etc.) in order for it to work correctly?
If you want to use builtin mixins.UpdateModelMixin, limit to PATCH and disable swagger from showing PUT you can use http_method_names
class UserViewSet(mixins.UpdateModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
http_method_names = ["patch"]
Instead of using mixins.UpdateModelMixin just define your own mixin that would perform patch only:
class UpdateModelMixin(object):
"""
Update a model instance.
"""
def partial_update(self, request, *args, **kwargs):
partial = True
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
A simple and straight forward approach:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
http_method_names = ['get', 'post', 'patch'] # <---------
Like this the PUT method will not be allowed.
I think a superior solution would be to use a custom router and disable the route for PUT. Then use your custom router for the viewsets.
class SimpleRouter(BaseRouter):
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes.
# Generated using #list_route decorator
# on methods of the viewset.
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes.
# Generated using #detail_route decorator on methods of the viewset.
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
]
^ The router implementation looks something like that. So you just need to inherit the SimpleRouter, or perhaps the DefaultRouter, and defines the routes class attribute how you want it. You can remove the mapping for 'put' in the Route(mapping={...}) completely, or you can define your own action to handle it and return the appropriate 400-something resonse.
Similar to #linovia's answer but using standard mixin:
from rest_framework.exceptions import MethodNotAllowed
class UpdateModelMixin(mixins.UpdateModelMixin, viewsets.GenericViewSet):
"""
update:
Update Model
"""
def update(self, *args, **kwargs):
raise MethodNotAllowed("POST", detail="Use PATCH")
def partial_update(self, request, *args, **kwargs):
# Override Partial Update Code if desired
return super().update(*args, **kwargs, partial=True)
Here's the solution I'm using:
class SomeViewSet(
mixins.UpdateModelMixin,
...
):
#swagger_auto_schema(auto_schema=None)
def update(self, request, *args, **kwargs):
"""Disabled full update functionality"""
partial = kwargs.get('partial', False) # This must be .get() not .pop()
if not partial:
raise exceptions.MethodNotAllowed(request.method)
return super(SomeViewSet, self).update(request, *args, **kwargs)
This will also disable it in drf-yasg UIs.
Solution similar to #EbramShehata's but for drf-spectacular (OpenAPI 3). This will disallow full updates (PUT) and also exclude that from the generated OpenAPI 3 schema.
class SomeViewSet(
mixins.UpdateModelMixin,
...
):
#extend_schema(exclude=True)
def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
"""Disallow full update (PUT) and allow partial update (PATCH)."""
if kwargs.get("partial", False): # Use .get() instead of .pop()
return super().update(request, args, kwargs)
raise MethodNotAllowed(request.method)

detail_route on viewset not following object level permissions

This is my ViewSet:
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly,)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, location=self.request.user.userextended.location)
#detail_route(methods=['post'])
def add(self, request, pk=None):
try:
page = Page.objects.get(pk=pk)
except:
content = {'Page': ['The page you are trying to add no longer exists.']}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
page.users.add(request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
And this is my IsOwnerOrReadOnly permission:
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Allow only the owner (and admin) of the object to make changes (i.e.
do PUT, PATCH, DELETE and POST requests. A user is an
owner of an object if the object has an attribute
called owner and owner is == request.user. If the
object is a User object or if the object does not have
an owner attribute, then return object == request.user.
"""
def has_permission(self, request, view):
print('In permission')
return True
def has_object_permission(self, request, view, obj):
print('In object level permission')
if request.method in permissions.SAFE_METHODS:
return True
if request.user.is_staff:
return True
try:
return obj.owner == request.user
except: # if obj does not have an owner property (e.g. users don't
# have owner properties).
return obj == request.user
The problem is, I can post to add-detail as an authenticated user even when I am not the owner of the page. When I do the post request, it only prints In permission twice and never prints In object level permission. My question is, since it is a detail_route and is clearly using a {lookup} object (see here which shows that it is using an object: http://www.django-rest-framework.org/api-guide/routers/), how come the has_object_permission() function from the permission class does not get called? How come only the regular has_permission() function gets called?
I'm hoping for someone to link to a documentation which verifies that even for detail_route, only has_permission gets called.
Edit: This is not a duplicate of Django rest framework ignores has_object_permission because I am using a ModelViewSet which inherits from GenericAPIView (as mentioned in the documentation: http://www.django-rest-framework.org/api-guide/viewsets/#modelviewset).
See this answer. There are some differences but the point is the same: check_object_permissions is not called.
Although you are inheriting from ModelViewSet you are not using its get_object in your add method to retrieve the page. It is get_object that calls check_object_permissions (not the router) for retrieve, update etc., so obviously it doesn't get called.
To fix it, do the following:
class PageViewSet(viewsets.ModelViewSet):
# ...
#detail_route(methods=['post'])
def add(self, request, pk=None):
page = self.get_object()
page.users.add(request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
or just do self.check_object_permissions(page) yourself somewhere in your implementation.

get_queryset method and ViewSets in django rest framework

I am doing exactly as the example states
here is my method
class FeedViewSet(viewsets.ModelViewSet):
model = Feed
serializer_class = FullFeedSerializer
def get_queryset(self):
user = request.user
queryset = Feed.objects.get_nearby(user)
return queryset
when i execute it, it says request not defined .. which actually isn't. the example at the rest framework's site also haven't defined request. what am i doing wrong?
The request object is available (on either REST framework's class based views, or Django's standard class based views) as self.request. You're missing the self. part of that.

Resources