I'm trying to enforce Authentication for access to my API, but the permission_classes = permissions.IsAuthenticated,) attribute of my ViewSet doesn't seem to be working. Because I try to access the User's profile later on when in get_queryset(), I'm getting errors like:
TypeError: 'AnonymousUser' object is not iterable"
I also have authentication set in my rest_framework settings:
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
And I also added to my dispatch method before doing the other business logic checks on the User profile:
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response(status=status.HTTP_401_UNAUTHORIZED)
That's not working either:
File "/lib/python3.5/site-packages/rest_framework/response.py", line 57, in rendered_content
assert renderer, ".accepted_renderer not set on Response"
AssertionError: .accepted_renderer not set on Response
Why do none of these authentication controls work?
Edit: here's a simplified version of get_queryset():
kwargs = {}
for k,v in self.request.query_params.items():
if not validate_that_key_is_an_actual_model_field(k):
return Response(status=status.HTTP_400_BAD_REQUEST)
kwargs[k] = v
return Product.objects.filter(fk_client=self.client)\
.filter(**kwargs)\
.order_by('uuid')\
.select_related('fk_user')
It turns out I was doing some other things in dispatch() that were executing before the super class's dispatch was called, and hence accessing the User profile before a Http401 could be returned by the super class. Additionally, because the super class's dispatch hadn't been called yet, the renderer hadn't been initialized properly (depending on the request's content headers) and so that was the source of the AssertionError: accepted_renderer not set on Response.
Related
I am using Django REST Framework with simple JWT, and I don't like how permission classes generate a response without giving me the final say on what gets sent to the client. With other class-based views not restricting permissions (login, registration, etc.), I have control over how I handle exceptions, and I can choose how the response data is structured.
However, anytime I introduce permission classes, undesired behavior occurs. My desired structure is best represented in my LoginView (see try/except block):
NON_FIELD_ERRORS_KEY = settings.REST_FRAMEWORK['NON_FIELD_ERRORS_KEY']
class LoginView(GenericAPIView):
"""
View for taking in an existing user's credentials and authorizing them if valid or denying access if invalid.
"""
serializer_class = LoginSerializer
def post(self, request):
"""
POST method for taking a token from a query string, checking if it is valid, and logging in the user if valid, or returning an error response if invalid.
"""
serializer = self.serializer_class(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except AuthenticationFailed as e:
return Response({NON_FIELD_ERRORS_KEY: [e.detail]}, status=e.status_code)
return Response(serializer.data, status=status.HTTP_200_OK)
However, what happens when I try to define a view using a permission class?
class LogoutView(GenericAPIView):
"""
View for taking in a user's access token and logging them out.
"""
serializer_class = LogoutSerializer
permission_classes = [IsAuthenticated]
def post(self, request):
"""
POST method for taking a token from a request body, checking if it is valid, and logging out the user if valid, or returning an error response if invalid.
"""
access = request.META.get('HTTP_AUTHORIZATION', '')
serializer = self.serializer_class(data={'access': access})
try:
serializer.is_valid(raise_exception=True)
except AuthenticationFailed as e:
return Response({NON_FIELD_ERRORS_KEY: [e.detail]}, status=e.status_code)
return Response(serializer.save(), status=status.HTTP_205_RESET_CONTENT)
When I go to test this, requests with an invalid Authorization header are handled outside the scope of post(), so execution never reaches the method at all. Instead, I am forced to deal with a response that is inconsistent with the rest of my project. Here's an example:
# Desired output
{
'errors': [
ErrorDetail(string='Given token not valid for any token type', code='token_not_valid')
]
}
# Actual output
{
'detail': ErrorDetail(string='Given token not valid for any token type', code='token_not_valid'),
'code': ErrorDetail(string='token_not_valid', code='token_not_valid'),
'messages': [
{
'token_class': ErrorDetail(string='AccessToken', code='token_not_valid'),
'token_type': ErrorDetail(string='access', code='token_not_valid'),
'message': ErrorDetail(string='Token is invalid or expired', code='token_not_valid')
}
]
}
Is there a simple way to change how these responses are formatted?
After stepping through the code, I found that the exceptions with which I was concerned were being raised in the last commented section of APIView.initial.
# rest_framework/views.py (lines 399-416)
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)
# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# Ensure that the incoming request is permitted
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
To generate a custom response, either create a custom implementation of APIView with an overloaded initial() method, or override the method in specific instances of APIView (or descendants such as GenericAPIView).
# my_app/views.py
def initial(self, request, *args, **kwargs):
"""
This method overrides the default APIView method so exceptions can be handled.
"""
try:
super().initial(request, *args, **kwargs)
except (AuthenticationFailed, InvalidToken) as exc:
raise AuthenticationFailed(
{NON_FIELD_ERRORS_KEY: [_('The provided token is invalid.')]},
'invalid')
except NotAuthenticated as exc:
raise NotAuthenticated(
{
NON_FIELD_ERRORS_KEY:
[_('Authentication credentials were not provided.')]
}, 'not_authenticated')
I'm using Django Rest Framework and want to be able to delete a Content instance via DELETE to /api/content/<int:pk>/. I don't want to implement any method to respond to GET requests.
When I include a .retrieve() method as follows, the DELETE request works:
class ContentViewSet(GenericViewSet):
def get_queryset(self):
return Content.objects.filter(user=self.request.user)
def retrieve(self, request, pk=None):
pass #this works, but I don't want .retrieve() at all
def delete(self, request, pk=None):
content = self.get_object()
#look up some info info here
content.delete()
return Response('return some info')
If I replace .retrieve() with RetrieveModelMixin it also works. However, if I remove both of these, which is what want to do, I get the following error.
django.urls.exceptions.NoReverseMatch: Reverse for 'content-detail' not found. 'content-detail' is not a valid view function or pattern name.
I haven't tested, but I assume the same thing would happen with PUT and PATCH.
My questions are:
How can I allow DELETE without implementing a .retrieve() method, and
Why can't DRF create the urlconf without .retrieve() implemented?
UPDATE: Failing test and complete error traceback caused by removing .retrieve() method
from rest_framework.test import APITestCase, APIClient
from myapp.models import Content
class ContentTestCase(APITestCase):
def setUp(self):
self.content = Content.objects.create(title='New content')
self.client = APIClient()
def test_DELETE_content(self):
url = reverse('content-detail', kwargs={'pk':self.content.pk})
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
Results in:
Traceback (most recent call last):
File "myproject/myapp/tests.py", line 548, in test_DELETE_content
url = reverse('content-detail', kwargs={'pk':self.content})
File "python3.6/site-packages/rest_framework/reverse.py", line 50, in reverse
url = _reverse(viewname, args, kwargs, request, format, **extra)
File "python3.6/site-packages/rest_framework/reverse.py", line 63, in _reverse
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
File "python3.6/site-packages/django/urls/base.py", line 90, in reverse
return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
File "python3.6/site-packages/django/urls/resolvers.py", line 636, in _reverse_with_prefix
raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'content-detail' not found. 'content-detail' is not a valid view function or pattern name.
How can I allow DELETE without implementing a .retrieve() method?
Just remove the retrieve() method from the view class. Which means, the GenericViewSet doesn't provide any HTTP Actions unless it's defined in your class. So, the following will be your code snippet,
class ContentViewSet(GenericViewSet):
def get_queryset(self):
return Content.objects.filter(user=self.request.user)
def delete(self, request, pk=None):
content = self.get_object()
# look up some info info here
content.delete()
return Response('return some info')
or you could use mixin classes here,
from rest_framework.mixins import DestroyModelMixin
class ContentViewSet(DestroyModelMixin, GenericViewSet):
def get_queryset(self):
return Content.objects.filter(user=self.request.user)
Why can't DRF create the urlconf without .retrieve() implemented?
I'm not sure how you've defined your URLs. When I tried with DRF Router, it's only creating the URL conf for defined actions.
You've got GET and DELETE actions on your end-point because of you'd defined the retrieve() method in your view class.
Hope this help :)
Wild guess here but did you use a SimpleRouter or a DefaultRouter to build your urlpatterns?
If so, that's your problem. The router uses a viewset and expects to have all methods implemented. More info here
What you can do is just add your url to urlpatterns like you would normally do on django using the .as_view() method.
My solution for part 1. is to include the mixin but restrict the http_method_names:
class ContentViewSet(RetrieveModelMixin, GenericViewSet):
http_method_names = ['delete']
...
However, I still don't know why I have to include RetrieveModelMixin at all.
I have a resource where i only want to allow a client to do a post request on the resource, thats why i use
class MyViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
in my viewset.
When i do a post request, it works as expected.
When i do a list request, it throws a 405 response, as expected.
When i do a retrieve, put, patch or delete method, it throws a 404 instead of a 405...why?
How can i make every single request return a 405 despite of the post request?
thanks and greetings!
Use http_method_names attribute
class MyViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
http_method_names = ['post']
# your code
There doesn't seem to be any reason to use a ViewSet if you only want to support a single action. Instead, use a CreateApiView with a specific URL pointing to it.
If you viewset doesn't have any detail methods, then the drf SimpleRouter will not create any url route for /api/basename/{id}/
So Django's url dispatcher will not match those urls at all, and returns a 404.
I don't think it makes sense to return 405 for every single method. That status implies that at least one method should be valid for a specific url.
You could add a dummy detail method, but just hand all requests over to the APIView 405 handler.
I think this should force the router to register detail urls for the viewset, and just return 405 for everything (possibly with the exception of OPTIONS).
class MyViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
def retrieve(self, request, *args, **kwargs):
return self.http_method_not_allowed(request, *args, **kwargs)
My application has several user types admin, user and manager.
I have defined an endpoint for a Resource, which has several prepend_urls.
eg: the endpoints would be
/Profile/search/
/Profile/shortview/
/Profile/
How can I limit access to the endpoints such that
/Profile/search/ is accessible to admin, manager
/Profile/shortview/ is accessible to all
/Profile/ is accessible to admin only
I have thought of using my own class but for authentication and authorization but think they are applied to the entire resource not individual prepend_url endpoints.
authorization = MyAuthorization()
authentication= MyAuthentication()
Any help is appreciated
I'm going to suppose you have been configured your prepend_urls and hence you have wrapped a function called dispatch_search, so something like this will raise an exception if user is unauthorized to use the endpoint:
def dispatch_search(self, request, *args, **kwargs):
# check authorization here
self._meta.authorization.is_authorized(request)
Edited from here below
When inheriting from the DjangoAuthorization class, you also can override the methods:
read_detail(self, object_list, bundle)
read_list(self, object_list, bundle)
to raise an exception if user should not be able to read an specific resource or the resource list itself.
And your MyAuthorization class:
from tastypie.exceptions import Unauthorized
from tastypie.authorization import DjangoAuthorization
class MyAuthorization(DjangoAuthorization):
def is_authorized(self, request):
if request.user.is_superuser and 'search' in request.path:
return True
# more business logic here to check the other endpoints
raise Unauthorized('Unauthorized :(')
def read_list(self, object_list, bundle):
self.is_authorized(bundle.request) # call your custom validation
# Fallback to the DjangoAuthorization read_list
return super(MyAuthorization, self).read_list(object_list, bundle)
Refer to the docs for a complete list of functions you can override to add more business logic: http://django-tastypie.readthedocs.org/en/latest/authorization.html#the-authorization-api
I would like to return some JSON responses back instead of just returning a header with an error code. Is there a way in tastypie to handle errors like that?
Figured it out eventually. Here's a good resource to look at, if anyone else needs it. http://gist.github.com/1116962
class YourResource(ModelResource):
def wrap_view(self, view):
"""
Wraps views to return custom error codes instead of generic 500's
"""
#csrf_exempt
def wrapper(request, *args, **kwargs):
try:
callback = getattr(self, view)
response = callback(request, *args, **kwargs)
if request.is_ajax():
patch_cache_control(response, no_cache=True)
# response is a HttpResponse object, so follow Django's instructions
# to change it to your needs before you return it.
# https://docs.djangoproject.com/en/dev/ref/request-response/
return response
except (BadRequest, ApiFieldError), e:
return HttpBadRequest({'code': 666, 'message':e.args[0]})
except ValidationError, e:
# Or do some JSON wrapping around the standard 500
return HttpBadRequest({'code': 777, 'message':', '.join(e.messages)})
except Exception, e:
# Rather than re-raising, we're going to things similar to
# what Django does. The difference is returning a serialized
# error message.
return self._handle_500(request, e)
return wrapper
You could overwrite tastypie's Resource method _handle_500(). The fact that it starts with an underscore indeed indicates that this is a "private" method and shouldn't be overwritten, but I find it a cleaner way than having to overwrite wrap_view() and replicate a lot of logic.
This is the way I use it:
from tastypie import http
from tastypie.resources import ModelResource
from tastypie.exceptions import TastypieError
class MyResource(ModelResource):
class Meta:
queryset = MyModel.objects.all()
fields = ('my', 'fields')
def _handle_500(self, request, exception):
if isinstance(exception, TastypieError):
data = {
'error_message': getattr(
settings,
'TASTYPIE_CANNED_ERROR',
'Sorry, this request could not be processed.'
),
}
return self.error_response(
request,
data,
response_class=http.HttpApplicationError
)
else:
return super(MyResource, self)._handle_500(request, exception)
In this case I catch all Tastypie errors by checking if exception is an instance of TastypieError and return a JSON reponse with the message "Sorry, this request could not be processed.". If it's a different exception, I call the parent _handle_500 using super(), which will create a django error page in development mode or send_admins() in production mode.
If you want to have a specific JSON response for a specific exception, just do the isinstance() check on a specific exception. Here are all the Tastypie exceptions:
https://github.com/toastdriven/django-tastypie/blob/master/tastypie/exceptions.py
Actually I think there should be a better/cleaner way to do this in Tastypie, so I opened a ticket on their github.