Only display forms when user has permissions. #159

This commit is contained in:
Tom Christie 2012-09-27 21:51:46 +01:00
parent 4d906938a9
commit ee36e4ab0c
7 changed files with 93 additions and 35 deletions

View File

@ -92,7 +92,6 @@ To implement a custom permission, override `BasePermission` and implement the `.
The method should return `True` if the request should be granted access, and `False` otherwise.
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
[authentication]: authentication.md
[throttling]: throttling.md

View File

@ -42,7 +42,8 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView):
Override default to add support for object-level permissions.
"""
obj = super(SingleObjectBaseView, self).get_object()
self.check_permissions(self.request, obj)
if not self.has_permission(self.request, obj):
self.permission_denied(self.request)
return obj

View File

@ -88,8 +88,8 @@ class MetadataMixin(object):
'parses': [parser.media_type for parser in self.parser_classes],
}
# TODO: Add 'fields', from serializer info.
# form = self.get_bound_form()
# if form is not None:
# serializer = self.get_serializer()
# if serializer is not None:
# field_name_types = {}
# for name, field in form.fields.iteritems():
# field_name_types[name] = field.__class__.__name__

View File

@ -5,6 +5,7 @@ Django REST framework also provides HTML and PlainText renderers that help self-
by serializing the output along with documentation regarding the View, output status and headers,
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
"""
import copy
import string
from django import forms
from django.template import RequestContext, loader
@ -193,7 +194,7 @@ class DocumentingHTMLRenderer(BaseRenderer):
format = 'html'
template = 'rest_framework/api.html'
def _get_content(self, view, request, obj, media_type):
def get_content(self, view, request, obj, media_type):
"""
Get the content as if it had been rendered by a non-documenting renderer.
@ -214,14 +215,31 @@ class DocumentingHTMLRenderer(BaseRenderer):
return content
def _get_form_instance(self, view, method):
def get_form(self, view, method, request):
"""
Get a form, possibly bound to either the input or output data.
In the absence on of the Resource having an associated form then
provide a form that can be used to submit arbitrary content.
"""
if not hasattr(self.view, 'get_serializer'): # No serializer, no form.
return
if not method in view.allowed_methods:
return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
return # Cannot use form overloading
temp = request._method
request._method = method.upper()
if not view.has_permission(request):
request._method = temp
return # Don't have permission
request._method = temp
if method == 'DELETE' or method == 'OPTIONS':
return True # Don't actually need to return a form
if not getattr(view, 'get_serializer', None):
return self.get_generic_content_form(view)
# We need to map our Fields to Django's Fields.
field_mapping = dict([
[serializers.FloatField.__name__, forms.FloatField],
@ -236,20 +254,20 @@ class DocumentingHTMLRenderer(BaseRenderer):
# Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
fields = {}
object, data = None, None
if hasattr(self.view, 'object'):
object = self.view.object
serializer = self.view.get_serializer(instance=object)
if getattr(view, 'object', None):
object = view.object
serializer = view.get_serializer(instance=object)
for k, v in serializer.fields.items():
if v.readonly:
continue
fields[k] = field_mapping[v.__class__.__name__]()
OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields)
if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted
if object and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted
data = serializer.data
form_instance = OnTheFlyForm(data)
return form_instance
def _get_generic_content_form(self, view):
def get_generic_content_form(self, view):
"""
Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
(Which are typically application/x-www-form-urlencoded)
@ -257,7 +275,8 @@ class DocumentingHTMLRenderer(BaseRenderer):
# If we're not using content overloading there's no point in supplying a generic form,
# as the view won't treat the form's value as the content of the request.
if not getattr(view.request, '_USE_FORM_OVERLOADING', False):
if not (api_settings.FORM_CONTENT_OVERRIDE
and api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None
# NB. http://jacobian.org/writing/dynamic-form-generation/
@ -272,11 +291,15 @@ class DocumentingHTMLRenderer(BaseRenderer):
contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types]
initial_contenttype = parsed_media_types[0]
self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
choices=contenttype_choices,
initial=initial_contenttype)
self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content',
widget=forms.Textarea)
self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField(
label='Content Type',
choices=contenttype_choices,
initial=initial_contenttype
)
self.fields[api_settings.FORM_CONTENT_OVERRIDE] = forms.CharField(
label='Content',
widget=forms.Textarea
)
# If either of these reserved parameters are turned off then content tunneling is not possible
if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None:
@ -310,10 +333,12 @@ class DocumentingHTMLRenderer(BaseRenderer):
request = view.request
response = view.response
content = self._get_content(view, request, obj, media_type)
content = self.get_content(view, request, obj, media_type)
put_form_instance = self._get_form_instance(self.view, 'put')
post_form_instance = self._get_form_instance(self.view, 'post')
put_form = self.get_form(view, 'PUT', request)
post_form = self.get_form(view, 'POST', request)
delete_form = self.get_form(view, 'DELETE', request)
options_form = self.get_form(view, 'OPTIONS', request)
name = self.get_name()
description = self.get_description()
@ -330,10 +355,12 @@ class DocumentingHTMLRenderer(BaseRenderer):
'name': name,
'version': VERSION,
'breadcrumblist': breadcrumb_list,
'allowed_methods': self.view.allowed_methods,
'available_formats': [renderer.format for renderer in self.view.renderer_classes],
'put_form': put_form_instance,
'post_form': post_form_instance,
'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in view.renderer_classes],
'put_form': put_form,
'post_form': post_form,
'delete_form': delete_form,
'options_form': options_form,
'api_settings': api_settings
})

View File

@ -89,7 +89,7 @@
</form>
{% endif %}
{% if 'OPTIONS' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %}
{% if options_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
@ -97,7 +97,7 @@
</form>
{% endif %}
{% if 'DELETE' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %}
{% if delete_form %}
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
{% csrf_token %}
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
@ -121,7 +121,7 @@
{% if response.status_code != 403 %}
{% if 'POST' in allowed_methods %}
{% if post_form %}
<form action="{{ request.get_full_path }}" method="POST" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
<fieldset>
<h2>POST: {{ name }}</h2>
@ -144,7 +144,7 @@
</form>
{% endif %}
{% if 'PUT' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %}
{% if put_form %}
<form action="{{ request.get_full_path }}" method="POST" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
<fieldset>
<h2>PUT: {{ name }}</h2>

View File

@ -2,8 +2,9 @@ import re
from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import status
from rest_framework import status, permissions
from rest_framework.compat import yaml
from rest_framework.response import Response
from rest_framework.views import APIView
@ -89,6 +90,34 @@ urlpatterns = patterns('',
)
class POSTDeniedPermission(permissions.BasePermission):
def has_permission(self, request, obj=None):
return request.method != 'POST'
class POSTDeniedView(APIView):
renderer_classes = (DocumentingHTMLRenderer,)
permission_classes = (POSTDeniedPermission,)
def get(self, request):
return Response()
def post(self, request):
return Response()
def put(self, request):
return Response()
class DocumentingRendererTests(TestCase):
def test_only_permitted_forms_are_displayed(self):
view = POSTDeniedView.as_view()
request = RequestFactory().get('/')
response = view(request).render()
self.assertNotContains(response, '>POST<')
self.assertContains(response, '>PUT<')
class RendererEndToEndTests(TestCase):
"""
End-to-end testing of renderers using an RendererMixin on a generic view.

View File

@ -169,13 +169,14 @@ class APIView(View):
conneg = self.content_negotiation_class()
return conneg.negotiate(request, renderers, self.format, force)
def check_permissions(self, request, obj=None):
def has_permission(self, request, obj=None):
"""
Check if request should be permitted.
Return `True` if the request should be permitted.
"""
for permission in self.get_permissions():
if not permission.has_permission(request, obj):
self.permission_denied(request)
return False
return True
def check_throttles(self, request):
"""
@ -197,7 +198,8 @@ class APIView(View):
Runs anything that needs to occur prior to calling the method handlers.
"""
self.format = self.get_format_suffix(**kwargs)
self.check_permissions(request)
if not self.has_permission(request):
self.permission_denied(request)
self.check_throttles(request)
self.renderer, self.accepted_media_type = self.perform_content_negotiation(request)