When using django-tastypie, I got some problems with security.
In my pinterest-clone application, I build a pin model.
A pin
is a image with a description and tags for it`
I want to build API for that model with some constrains:
- Everyone can see pins of other people
- Only author of pin can delete/modify his owner pin, other people are not allow to do this action
So the API should be
1. Get list pin
* HTTP Method: GET
* URL endpoint: http://<domain>/api/pin/
* Return:
[
objects : [{
<pin infomation>
}]
]
2. Delete a pin
* HTTP Method: DELETE
* URL endpoint: http://<domain>/api/pin/<pin_id>/
* Return:
+ { "error": "Pin not found" } if dont have <pin_id> in database
+ { "error": "Authorization error" } if user request this API is not owner of <pin_id>
+ Nothing if delete pin successfully
3. Modify a pin
* HTTP Method: DELETE
* URL endpoint: http://<domain>/api/pin/<pin_id>/
* POST params:
{
"description": <string>
"tags": List of string
}
* Return:
+ { "error": "Pin not found" } if dont have <pin_id> in database
+ { "error": "Authorization error" } if user request this API is not owner of <pin_id>
+ Nothing if modify pins successful
To solve security issue with django-tastypie, I subclass tastypie.authorization.Authorization
class.
And modify the method apply_limits
in this custom class
Here is the source code
from tastypie.resources import ModelResource
from tastypie.exceptions import BadRequest
from tastypie.serializers import Serializer
from tastypie.authorization import Authorization
from django.contrib.auth.models import User
from pinry.pins.models import Pin
from pinry.pins.models import Like
from pinry.pins.models import Comment
from pinry.core.models import Member
class PinAuthorization(Authorization):
def is_authorized(self, request, object=None):
# only logged in user will can modify pins
if request.method in ("DELETE", "PUT"):
if not request.user.is_authenticated():
raise BadRequest(json.dumps({"error": "Authorization error"}))
return True
def apply_limits(self, request, object_list=None):
# only allow delete/modify pin belong to this user
if request.method in ("DELETE", "PUT"):
filter_list = object_list.filter(submitter=request.user.get_profile())
if not filter_list:
raise BadRequest(json.dumps({"error": "Authorization error"}))
return filter_list
return object_list
class PinResource(ModelResource):
class Meta:
queryset = Pin.objects.all()
resource_name = 'pin'
list_allowed_method = ["GET"]
details_allowd_method = ["GET", "PUT", "DELETE"]
include_resource_uri = False
authorization = PinAuthorization()
serializer = Serializer(["json"])
filtering = {
'published': ['gt'],
}
There are some note in my code:
- In PinResource class, i only accept GET method for list request and GET, PUT, DELETE for detail method
- I set default serializer is json format, so i dont need pass paramter format=json in every request
- To return error in response, i raise BadRequest exception in 2 functions
is_authorized
andapply_limits
. Type of exception (BadRequest) is very important because tastypie only handle that exception when process your request.
Here is the code i got from lastest source code of tastypie on github
# file tastypie/resource class Resource
def wrap_view(self, view):
"""
Wraps methods so they can be called in a more functional way as well
as handling exceptions better.
Note that if ``BadRequest`` or an exception with a ``response`` attr
are seen, there is special handling to either present a message back
to the user or return the response traveling with the exception.
"""
@csrf_exempt
def wrapper(request, *args, **kwargs):
try:
callback = getattr(self, view)
response = callback(request, *args, **kwargs)
# Our response can vary based on a number of factors, use
# the cache class to determine what we should ``Vary`` on so
# caches won't return the wrong (cached) version.
varies = getattr(self._meta.cache, "varies", [])
if varies:
patch_vary_headers(response, varies)
if self._meta.cache.cacheable(request, response):
if self._meta.cache.cache_control():
# If the request is cacheable and we have a
# ``Cache-Control`` available then patch the header.
patch_cache_control(response, **self._meta.cache.cache_control())
if request.is_ajax() and not response.has_header("Cache-Control"):
# IE excessively caches XMLHttpRequests, so we're disabling
# the browser cache here.
# See http://www.enhanceie.com/ie/bugs.asp for details.
patch_cache_control(response, no_cache=True)
return response
except (BadRequest, fields.ApiFieldError), e:
return http.HttpBadRequest(e.args[0])
except ValidationError, e:
return http.HttpBadRequest(', '.join(e.messages))
except Exception, e:
if hasattr(e, 'response'):
return e.response
# A real, non-expected exception.
# Handle the case where the full traceback is more helpful
# than the serialized error.
if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False):
raise
# Re-raise the error to get a proper traceback when the error
# happend during a test case
if request.META.get('SERVER_NAME') == 'testserver':
raise
# 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
wrap_view
is method was called when tastypie get request from client.
In this code, tastypie only return HttpBadRequest for BadRequest and ApiFieldError exception, other Exception will handle by 500 internal error request