how to access POST data inside tastypie custom Authentication
Asked Answered
J

4

12

I'm trying to write custom Authentication in tastypie. Basically, I want to do the authentication using the post parameters and I don't want to use the django auth at all, so my code looks something like:

class MyAuthentication(Authentication):
   def is_authenticated(self, request, **kwargs):
       if request.method == 'POST':
           token = request.POST['token']
           key = request.POST['key']
           return is_key_valid(token,key)

This is more or less the idea. The problem is that I keep getting the following error:

"error_message": "You cannot access body after reading from request's data stream"

I understand that this is related to the fact that I'm accessing the POST, but I could not figure if there is a way to solve it. Any ideas? Thanks.

EDIT: Maybe I forgot the mention the most important thing. I'm handling form data using a trick I found in github. My resource derives from multipart resource

class MultipartResource(object):
    def deserialize(self, request, data, format=None):
        if not format:
            format = request.META.get('CONTENT_TYPE', 'application/json')

        if format == 'application/x-www-form-urlencoded':
            return request.POST

        if format.startswith('multipart'):
            data = request.POST.copy()
            data.update(request.FILES)
            return data
        return super(MultipartResource, self).deserialize(request, data, format)
Julee answered 20/9, 2012 at 23:22 Comment(2)
There's nothing wrong with accessing request.POST... Django saves the POST variables on the request object for you to do exactly that. I don't think there's a problem with this code directly... I suspect TastyPie's doing something odd - maybe check your request has the correct Content-type headers, etc? As a side point, it's a good idea to either use e.g. token = request.POST.get('token', some_default) or catch and deal with the KeyError thrown if that parameter doesn't exist - maybe by returning an HTTP 401.Saucy
had a similar situation with django rest frame work, I cannot access request.body, but the data is available in request.dataRoma
C
13

The problem is the Content-Type in your request' headers isn't correctly set. [Reference]

Tastypie only recognizes xml, json, yaml and bplist. So when sending the POST request, you need to set Content-Type in the request headers to either one of them (eg., application/json).

EDIT:

It seems like you are trying to send a multipart form with files through Tastypie.

A little background on Tastypie's file upload support by Issac Kelly for roadmap 1.0 final (hasn't released yet):

  1. Implement a Base64FileField which accepts base64 encoded files (like the one in issue #42) for PUT/POST, and provides the URL for GET requests. This will be part of the main tastypie repo.
  2. We'd like to encourage other implementations to implement as independent projects. There's several ways to do this, and most of them are slightly finicky, and they all have different drawbacks, We'd like to have other options, and document the pros and cons of each

That means for now at least, Tastypie does not officially support multipart file upload. However, there are forks in the wild that are supposedly working well, this is one of them. I haven't tested it though.


Now let me try to explain why you are encountering that error.

In Tastypie resource.py, line 452:

def dispatch(self, request_type, request, **kwargs):
    """
    Handles the common operations (allowed HTTP method, authentication,
    throttling, method lookup) surrounding most CRUD interactions.
    """
    allowed_methods = getattr(self._meta, "%s_allowed_methods" % request_type, None)

    if 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
        request.method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']

    request_method = self.method_check(request, allowed=allowed_methods)
    method = getattr(self, "%s_%s" % (request_method, request_type), None)

    if method is None:
        raise ImmediateHttpResponse(response=http.HttpNotImplemented())

    self.is_authenticated(request)
    self.is_authorized(request)
    self.throttle_check(request)

    # All clear. Process the request.
    request = convert_post_to_put(request)
    response = method(request, **kwargs)

    # Add the throttled request.
    self.log_throttled_access(request)

    # If what comes back isn't a ``HttpResponse``, assume that the
    # request was accepted and that some action occurred. This also
    # prevents Django from freaking out.
    if not isinstance(response, HttpResponse):
        return http.HttpNoContent()

    return response

convert_post_to_put(request) is called from here. And here is the code for convert_post_to_put:

# Based off of ``piston.utils.coerce_put_post``. Similarly BSD-licensed.
# And no, the irony is not lost on me.
def convert_post_to_VERB(request, verb):
    """
    Force Django to process the VERB.
    """
    if request.method == verb:
        if hasattr(request, '_post'):
            del(request._post)
            del(request._files)

        try:
            request.method = "POST"
            request._load_post_and_files()
            request.method = verb
        except AttributeError:
            request.META['REQUEST_METHOD'] = 'POST'
            request._load_post_and_files()
            request.META['REQUEST_METHOD'] = verb
        setattr(request, verb, request.POST)

    return request


def convert_post_to_put(request):
    return convert_post_to_VERB(request, verb='PUT')

And this method isn't really intended to handled multipart as it has side-effect of preventing any further accesses to request.body because _load_post_and_files() method will set _read_started flag to True:

Django request.body and _load_post_and_files():

@property
def body(self):
    if not hasattr(self, '_body'):
        if self._read_started:
            raise Exception("You cannot access body after reading from request's data stream")
        try:
            self._body = self.read()
        except IOError as e:
            six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2])
        self._stream = BytesIO(self._body)
    return self._body

def read(self, *args, **kwargs):
    self._read_started = True
    return self._stream.read(*args, **kwargs)

def _load_post_and_files(self):
    # Populates self._post and self._files
    if self.method != 'POST':
        self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict()
        return
    if self._read_started and not hasattr(self, '_body'):
        self._mark_post_parse_error()
        return

    if self.META.get('CONTENT_TYPE', '').startswith('multipart'):
        if hasattr(self, '_body'):
            # Use already read data
            data = BytesIO(self._body)
        else:
            data = self
        try:
            self._post, self._files = self.parse_file_upload(self.META, data)
        except:
            # An error occured while parsing POST data. Since when
            # formatting the error the request handler might access
            # self.POST, set self._post and self._file to prevent
            # attempts to parse POST data again.
            # Mark that an error occured. This allows self.__repr__ to
            # be explicit about it instead of simply representing an
            # empty POST
            self._mark_post_parse_error()
            raise
    else:
        self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict()

So, you can (though probably shouldn't) monkey-patch Tastypie's convert_post_to_VERB() method by setting request._body by calling request.body and then immediately set _read_started=False so that _load_post_and_files() will read from _body and won't set _read_started=True:

def convert_post_to_VERB(request, verb):
    """
    Force Django to process the VERB.
    """
    if request.method == verb:
        if hasattr(request, '_post'):
            del(request._post)
            del(request._files)

        request.body  # now request._body is set
        request._read_started = False  # so it won't cause side effects

        try:
            request.method = "POST"
            request._load_post_and_files()
            request.method = verb
        except AttributeError:
            request.META['REQUEST_METHOD'] = 'POST'
            request._load_post_and_files()
            request.META['REQUEST_METHOD'] = verb
        setattr(request, verb, request.POST)

    return request
Cutting answered 25/9, 2012 at 4:54 Comment(4)
You are right, however, I'm using a parent class to handle this. Forgot to mention this in the question. thanksJulee
@Julee there's some problem with Tastypie multipart uploading won't fix, the code you are using will not fix it completely. See my updated answer for explanation.Cutting
I just had the same problem as the OP, and based on your excellent explanation, I tried the following very simple fix: in the MyAuthentication class, just before returning, add the line: request._read_started = False. This seems to work.Britneybritni
Why would convert_post_to_VERB ever fire, if request.method == verb will never be True for POST requests? request.method is POST, whereas verb is PUT. Isn't the whole method a no-op?Pumping
P
2

You say you need custom auth which is fine but please consider using the Authorization header instead. By using POST you force Django to parse the entire payload assuming the data is either urlencoded or multipart form encoded. This effectively makes it impossible to use non-form payloads such as JSON or YAML.

class MyAuthentication(Authentication):
    def is_authenticated(self, request, **kwargs):
        auth_info = request.META.get('HTTP_AUTHORIZATION')
        # ...
Pomegranate answered 27/9, 2012 at 18:47 Comment(0)
C
0

This error occurs when you access request.body (or request.raw_post_data if you're still on Django 1.3) a second time or, I believe, if you access it after having accessed the POST, GET, META or COOKIES attributes.

Tastypie will access the request.body (raw_post_data) attribute when processing PUT or PATCH requests.

With this in mind and without knowing more detail, I would:

  • Check if this only happens for POST/PUTs. If so, then you would have to do some overriding of some tastypie methods or abandon your approach for authentication.
  • Look for places in your code where you access request.body (raw_post_data)
  • Look for calls on 3rd party modules (perhaps a middleware) that might try to access body/raw_post_data

Hope this helps!

Chanteuse answered 24/9, 2012 at 20:22 Comment(0)
P
0

I've created a utility method that works well for me. Though I am not sure how this affects the underlying parts of Django, it works:

import io


def copy_body(request):
    data = getattr(request, '_body', request.body)
    request._body = data
    request._stream = io.BytesIO(data)
    request._files = None
    return data

I use it in a middleware to add a JSON attribute to request: https://gist.github.com/antonagestam/9add2d69783287025907

Penstock answered 12/6, 2014 at 15:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.