how to access POST data inside tastypie custom Authentication - django

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)

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):
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.
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

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')
# ...

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!

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

Related

How to test django non-REST POST endpoint?

I'm having trouble testing one of my endpoints:
#require_http_methods(["POST"])
def store(request):
try:
body_unicode = request.body.decode('utf-8')
body = ast.literal_eval(body_unicode)
new_short_url = body['short_url']
original_url = body['original_url']
check_parameters(new_short_url, original_url)
Url.objects.create(short_url=new_short_url, original_url=original_url)
return HttpResponse('Created', status=201)
except KeyError as error:
return HttpResponse('Missing {}'.format(error.args), status=400)
except (AttributeError, IntegrityError, ValidationError) as error:
return HttpResponse(error.args, status=400)
As you can see, this endpoint only accepts POST requests and when trying to pass data from my tests, it arrives in the request.body, so I implemented my logic to get the data from there:
def test_create_url_ok(self):
creation_data = {
"short_url": "ab",
"original_url": "https://stackoverflow.com/"
}
response = self.client.post(reverse('store'), data=creation_data, content_type="application/json")
self.assertEqual(response.status_code, 201)
This works, but the problem is that when sending requests from my templates, data is not in the request.body, but in the request.POST. How to send data in the request.POST from my tests?
So, the thing is that vanilla django accepts data from request.POST when the content_type='multipart/form-data'. Since I was using 'json' it wasn't working (though it works when using DjangoRestFramework views):
When using default django tests, this content_type is the default, so you don't need to specify any:

How to send cookies in CreateView?

I am trying to implement signed cookie sending in CreateView, but I have encountered with the trouble. Following code works in UpdateView but in CreateView we dont have self.object in render_to_response method and basically we cant get a pk there or at least I dont know how to do it.
Question is how to get pk or id of a freshly created object or maybe alternatively in which method I could move my code to get access to pk from there?
Thanks.
def render_to_response(self, context, **response_kwargs):
response = CreateView.render_to_response(self, context, **response_kwargs)
existing_allowed_comments = self.request.get_signed_cookie('allowed_comments', default=None)
if not self.request.user.is_authenticated:
if existing_allowed_comments and str(self.object.pk) not in \
existing_allowed_comments:
response.set_signed_cookie('allowed_comments',
", ".join([existing_allowed_comments, str(self.object.pk)])
elif not existing_allowed_comments:
response.set_signed_cookie('allowed_comments', self.object.pk
return response
method should add pk of created objects to signed cookies in case user is not authenticated.
self.get_object() doesn't work as well – 404
You may be better to override the form_valid() method for this. This method creates an object from the validated form data so you will have access to self.object after calling the method in the base class:
def form_valid(self, form):
response = super().form_valid(form)
existing_allowed_comments = self.request.get_signed_cookie('allowed_comments', default=None)
if not self.request.user.is_authenticated:
if existing_allowed_comments and str(self.object.pk) not in \
existing_allowed_comments:
response.set_signed_cookie('allowed_comments',
", ".join([existing_allowed_comments, str(self.object.pk)])
elif not existing_allowed_comments:
response.set_signed_cookie('allowed_comments', self.object.pk
return response
Note that if the form is not valid because of bad data, no object will be created and this method will not get called.

Do I need to use request.is_ajax() in my views?

I am learning to use Ajax with Django, many tutorials simply check if request.method == 'GET' or POST. I am curious for what do we need .is_ajax() then. Is it normal no to use it or tutorials just show basic concepts?
I am curious for what do we need .is_ajax() then. Is it normal no to
use it or tutorials just show basic concepts?
Yes, it is totally normal not to use is_ajax. Most of the time what you care about in your views is the HTTP verb (e.g. GET, POST, PATCH..).
However there are certain cases where you want to know if the request is an AJAX request. Why? because you might want to return a different result depending if the request is ajax or not.
The most common use for this solution is PJAX. When you use a pjax technology, if the request is not an ajax request you render the entire page, whereas if the request comes from ajax you render only a partial of the page. Then the partial page is added in the correct place in the webpage by some sort of lib, such as https://github.com/defunkt/jquery-pjax.
For example, this is a mixing I wrote to use Pjax in django:
import os
from django.views.generic.base import TemplateResponseMixin
class PJAXResponseMixin(TemplateResponseMixin):
pjax_template_name = None
pjax_suffix = "pjax"
pjax_url = True
def get_context_data(self, **kwargs):
context = super(TemplateResponseMixin, self).get_context_data(**kwargs)
context['inner_template'] = self.pjax_template_name
return context
def get_template_names(self):
names = super(PJAXResponseMixin, self).get_template_names()
if self.request.is_ajax():
if self.pjax_template_name:
names = [self.pjax_template_name]
else:
names = self._pjaxify_template_var(names)
return names
def get(self, request, *args, **kwargs):
response = super(PJAXResponseMixin, self).get(request, *args, **kwargs)
if sel
f.pjax_url :
response['X-PJAX-URL'] = self.request.path
return response
def _pjaxify_template_var(self, template_var):
if isinstance(template_var, (list, tuple)):
template_var = type(template_var)(self._pjaxify_template_name(name) for name in template_var)
elif isinstance(template_var, basestring):
template_var = self._pjaxify_template_name(template_var)
return template_var
def _pjaxify_template_name(self, name):
container = self.request.META.get('HTTP_X_PJAX_CONTAINER', False)
if container is not False:
name = _add_suffix(name, clean_container_name(container))
return _add_suffix(name, self.pjax_suffix)
#################################################
# HELPER METHODS #
#################################################
def clean_container_name(name):
return name.replace('#', '')
def _add_suffix(name, suffix):
if "." in name:
file_name, file_extension = os.path.splitext(name)
name = "{0}-{1}{2}".format(file_name, suffix, file_extension)
else:
name += "-{0}".fomat(suffix)
return name
Basically, this mixing renders the default template if the request is not an ajax request. Whereas if the request is AJAX, it renders the pjax_template, if there is one, or the name of the default template prefixed with pjax.

I have a middleware where I a want to log every request/response. How can I access to POST data?

I have this middleware
import logging
request_logger = logging.getLogger('api.request.logger')
class LoggingMiddleware(object):
def process_response(self, request, response):
request_logger.log(logging.DEBUG,
"GET: {}. POST: {} response code: {}. response "
"content: {}".format(request.GET, request.DATA,
response.status_code,
response.content))
return response
The problem is that request in process_response method has no .POST nor .DATA nor .body. I am using django-rest-framework and my requests has Content-Type: application/json
Note, that if I put logging to process_request method - it has .body and everything I need. However, I need both request and response in a single log entry.
Here is complete solution I made
"""
Api middleware module
"""
import logging
request_logger = logging.getLogger('api.request.logger')
class LoggingMiddleware(object):
"""
Provides full logging of requests and responses
"""
_initial_http_body = None
def process_request(self, request):
self._initial_http_body = request.body # this requires because for some reasons there is no way to access request.body in the 'process_response' method.
def process_response(self, request, response):
"""
Adding request and response logging
"""
if request.path.startswith('/api/') and \
(request.method == "POST" and
request.META.get('CONTENT_TYPE') == 'application/json'
or request.method == "GET"):
request_logger.log(logging.DEBUG,
"GET: {}. body: {} response code: {}. "
"response "
"content: {}"
.format(request.GET, self._initial_http_body,
response.status_code,
response.content), extra={
'tags': {
'url': request.build_absolute_uri()
}
})
return response
Note, this
'tags': {
'url': request.build_absolute_uri()
}
will allow you to filter by url in sentry.
Andrey's solution will break on concurrent requests. You'd need to store the body somewhere in the request scope and fetch it in the process_response().
class RequestLoggerMiddleware(object):
def process_request(self, request):
request._body_to_log = request.body
def process_response(self, request, response):
if not hasattr(request, '_body_to_log'):
return response
msg = "method=%s path=%s status=%s request.body=%s response.body=%s"
args = (request.method,
request.path,
response.status_code,
request._body_to_log,
response.content)
request_logger.info(msg, *args)
return response
All answers above have one potential problem -- big request.body passed to the server. In Django request.body is a property. (from framework)
#property
def body(self):
if not hasattr(self, '_body'):
if self._read_started:
raise RawPostDataException("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
Django framework access body directly only in one case. (from framework)
elif self.META.get('CONTENT_TYPE', '').startswith('application/x-www-form-urlencoded'):
As you can see, property body read the entire request into memory. As a result, your server can simply crash. Moreover, it becomes vulnerable to DoS attack.
In this case I would suggest using another method of HttpRequest class. (from framework)
def readlines(self):
return list(iter(self))
So, you no longer need to do this
def process_request(self, request):
request._body_to_log = request.body
you can simply do:
def process_response(self, request, response):
msg = "method=%s path=%s status=%s request.body=%s response.body=%s"
args = (request.method,
request.path,
response.status_code,
request.readlines(),
response.content)
request_logger.info(msg, *args)
return response
EDIT: this approach with request.readlines() has problems. Sometimes it does not log anything.
It's frustrating and surprising that there is no easy-to-use request logging package in Django.
So I created one myself. Check it out: https://github.com/rhumbixsf/django-request-logging.git
Uses the logging system so it is easy to configure. This is what you get with DEBUG level:
GET/POST request url
POST BODY if any
GET/POST request url - response code
Response body
It is like accessing the form data to create a new form.
You must use request.POST for this (perhaps request.FILES is something you'd log as well).
class LoggingMiddleware(object):
def process_response(self, request, response):
request_logger.log(logging.DEBUG,
"GET: {}. POST: {} response code: {}. response "
"content: {}".format(request.GET, request.POST,
response.status_code,
response.content))
return response
See Here for request properties.
You can use like below:
"""
Middleware to log requests and responses.
"""
import socket
import time
import json
import logging
request_logger = logging.getLogger(__name__)
class RequestLogMiddleware:
"""Request Logging Middleware."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
log_data = {}
# add request payload to log_data
req_body = json.loads(request.body.decode("utf-8")) if request.body else {}
log_data["request_body"] = req_body
# request passes on to controller
response = self.get_response(request)
# add response payload to our log_data
if response and response["content-type"] == "application/json":
response_body = json.loads(response.content.decode("utf-8"))
log_data["response_body"] = response_body
request_logger.info(msg=log_data)
return response
# Log unhandled exceptions as well
def process_exception(self, request, exception):
try:
raise exception
except Exception as e:
request_logger.exception("Unhandled Exception: " + str(e))
return exception
You can also check this out - log requests via middleware explains this
Also note, that response.content returns bytestring and not unicode string so if you need to print unicode, you need to call response.content.decode("utf-8").
You cannot access request.POST (or equivalently request.body) in the process_response part of the middleware. Here is a ticket raising the issue. Though you can have it in the process_request part. The previous answers give a class-based middleware. Django 2.0+ and 3.0+ allow function based middlewares.
from .models import RequestData # Model that stores all the request data
def requestMiddleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
try : metadata = request.META ;
except : metadata = 'no data'
try : data = request.body ;
except : data = 'no data'
try : u = str(request.user)
except : u = 'nouser'
response = get_response(request)
w = RequestData.objects.create(userdata=u, metadata=metadata,data=data )
w.save()
return response
return middleware
Model RequestData looks as follows -
class RequestData(models.Model):
time = models.DateTimeField(auto_now_add=True)
userdata = models.CharField(max_length=10000, default=' ')
data = models.CharField(max_length=20000, default=' ')
metadata = models.CharField(max_length=20000, default=' ')

How do I filter sensitive Django POST parameters out of Sentry error reports?

To quote the Django docs:
#sensitive_post_parameters('pass_word', 'credit_card_number')
def record_user_profile(request):
UserProfile.create(user=request.user,
password=request.POST['pass_word'],
credit_card=request.POST['credit_card_number'],
name=request.POST['name'])
In the above example, the values for the pass_word and credit_card_number POST parameters will be hidden and replaced with stars (******) in the request’s representation inside the error reports, whereas the value of the name parameter will be disclosed.
To systematically hide all POST parameters of a request in error reports, do not provide any argument to the sensitive_post_parameters decorator:
#sensitive_post_parameters()
def my_view(request):
...
As a test, I added the following code to my Django 1.6 application:
views.py:
#sensitive_post_parameters('sensitive')
def sensitive(request):
if request.method == 'POST':
raise IntegrityError(unicode(timezone.now()))
return render(request, 'sensitive-test.html',
{'form': forms.SensitiveParamForm()})
forms.py:
class SensitiveParamForm(forms.Form):
not_sensitive = forms.CharField(max_length=255)
sensitive = forms.CharField(max_length=255)
When I submit this form via POST, I can see the values of both fields (including sensitive) clear as day in the Sentry report.
What am I doing wrong here? I'm using Django 1.6 and Raven 3.5.2.
Thanks in advance for your help!
Turns out that this stemmed from a bug in Django itself!
If you haven't changed DEFAULT_EXCEPTION_REPORTER_FILTER in your settings file, you get the default filter of SafeExceptionReporterFilter.
If you've used the sensitive_post_parameters decorator, this will result in your calling SafeExceptionReporterFilter's get_post_parameters method:
def get_post_parameters(self, request):
"""
Replaces the values of POST parameters marked as sensitive with
stars (*********).
"""
if request is None:
return {}
else:
sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
if self.is_active(request) and sensitive_post_parameters:
cleansed = request.POST.copy()
if sensitive_post_parameters == '__ALL__':
# Cleanse all parameters.
for k, v in cleansed.items():
cleansed[k] = CLEANSED_SUBSTITUTE
return cleansed
else:
# Cleanse only the specified parameters.
for param in sensitive_post_parameters:
if param in cleansed:
cleansed[param] = CLEANSED_SUBSTITUTE
return cleansed
else:
return request.POST
The problem with the above is that while it will correctly return a QuerySet with the sensitive POST parameters set to CLEANSED_SUBSTITUTE ('********************')...it won't in any way alter request.body.
This is a problem when working with Raven/Sentry for Django, because it turns out that the get_data_from_request method of Raven's DjangoClient first attempts to get the request's POST parameters from request.body:
def get_data_from_request(self, request):
[snip]
if request.method != 'GET':
try:
data = request.body
except Exception:
try:
data = request.raw_post_data
except Exception:
# assume we had a partial read.
try:
data = request.POST or '<unavailable>'
except Exception:
data = '<unavailable>'
else:
data = None
[snip]
The fastest fix turned out to just involve subclassing DjangoClient and manually replacing its output with the cleansed QuerySet produced by SafeExceptionReporterFilter:
from django.views.debug import SafeExceptionReporterFilter
from raven.contrib.django.client import DjangoClient
class SafeDjangoClient(DjangoClient):
def get_data_from_request(self, request):
request.POST = SafeExceptionReporterFilter().get_post_parameters(request)
result = super(SafeDjangoClient, self).get_data_from_request(request)
result['sentry.interfaces.Http']['data'] = request.POST
return result