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. 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 [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
[authentication]: authentication.md [authentication]: authentication.md
[throttling]: throttling.md [throttling]: throttling.md

View File

@ -42,7 +42,8 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView):
Override default to add support for object-level permissions. Override default to add support for object-level permissions.
""" """
obj = super(SingleObjectBaseView, self).get_object() 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 return obj

View File

@ -88,8 +88,8 @@ class MetadataMixin(object):
'parses': [parser.media_type for parser in self.parser_classes], 'parses': [parser.media_type for parser in self.parser_classes],
} }
# TODO: Add 'fields', from serializer info. # TODO: Add 'fields', from serializer info.
# form = self.get_bound_form() # serializer = self.get_serializer()
# if form is not None: # if serializer is not None:
# field_name_types = {} # field_name_types = {}
# for name, field in form.fields.iteritems(): # for name, field in form.fields.iteritems():
# field_name_types[name] = field.__class__.__name__ # 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, 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. and providing forms and links depending on the allowed methods, renderers and parsers on the View.
""" """
import copy
import string import string
from django import forms from django import forms
from django.template import RequestContext, loader from django.template import RequestContext, loader
@ -193,7 +194,7 @@ class DocumentingHTMLRenderer(BaseRenderer):
format = 'html' format = 'html'
template = 'rest_framework/api.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. Get the content as if it had been rendered by a non-documenting renderer.
@ -214,14 +215,31 @@ class DocumentingHTMLRenderer(BaseRenderer):
return content 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. Get a form, possibly bound to either the input or output data.
In the absence on of the Resource having an associated form then In the absence on of the Resource having an associated form then
provide a form that can be used to submit arbitrary content. provide a form that can be used to submit arbitrary content.
""" """
if not hasattr(self.view, 'get_serializer'): # No serializer, no form. if not method in view.allowed_methods:
return 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. # We need to map our Fields to Django's Fields.
field_mapping = dict([ field_mapping = dict([
[serializers.FloatField.__name__, forms.FloatField], [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 # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
fields = {} fields = {}
object, data = None, None object, data = None, None
if hasattr(self.view, 'object'): if getattr(view, 'object', None):
object = self.view.object object = view.object
serializer = self.view.get_serializer(instance=object) serializer = view.get_serializer(instance=object)
for k, v in serializer.fields.items(): for k, v in serializer.fields.items():
if v.readonly: if v.readonly:
continue continue
fields[k] = field_mapping[v.__class__.__name__]() fields[k] = field_mapping[v.__class__.__name__]()
OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) 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 data = serializer.data
form_instance = OnTheFlyForm(data) form_instance = OnTheFlyForm(data)
return form_instance 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 Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
(Which are typically application/x-www-form-urlencoded) (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, # 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. # 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 return None
# NB. http://jacobian.org/writing/dynamic-form-generation/ # 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] contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types]
initial_contenttype = parsed_media_types[0] initial_contenttype = parsed_media_types[0]
self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField(
label='Content Type',
choices=contenttype_choices, choices=contenttype_choices,
initial=initial_contenttype) initial=initial_contenttype
self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content', )
widget=forms.Textarea) 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 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: 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 request = view.request
response = view.response 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') put_form = self.get_form(view, 'PUT', request)
post_form_instance = self._get_form_instance(self.view, 'post') 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() name = self.get_name()
description = self.get_description() description = self.get_description()
@ -330,10 +355,12 @@ class DocumentingHTMLRenderer(BaseRenderer):
'name': name, 'name': name,
'version': VERSION, 'version': VERSION,
'breadcrumblist': breadcrumb_list, 'breadcrumblist': breadcrumb_list,
'allowed_methods': self.view.allowed_methods, 'allowed_methods': view.allowed_methods,
'available_formats': [renderer.format for renderer in self.view.renderer_classes], 'available_formats': [renderer.format for renderer in view.renderer_classes],
'put_form': put_form_instance, 'put_form': put_form,
'post_form': post_form_instance, 'post_form': post_form,
'delete_form': delete_form,
'options_form': options_form,
'api_settings': api_settings 'api_settings': api_settings
}) })

View File

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

View File

@ -2,8 +2,9 @@ import re
from django.conf.urls.defaults import patterns, url, include from django.conf.urls.defaults import patterns, url, include
from django.test import TestCase 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.compat import yaml
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView 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): class RendererEndToEndTests(TestCase):
""" """
End-to-end testing of renderers using an RendererMixin on a generic view. 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() conneg = self.content_negotiation_class()
return conneg.negotiate(request, renderers, self.format, force) 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(): for permission in self.get_permissions():
if not permission.has_permission(request, obj): if not permission.has_permission(request, obj):
self.permission_denied(request) return False
return True
def check_throttles(self, request): def check_throttles(self, request):
""" """
@ -197,7 +198,8 @@ class APIView(View):
Runs anything that needs to occur prior to calling the method handlers. Runs anything that needs to occur prior to calling the method handlers.
""" """
self.format = self.get_format_suffix(**kwargs) 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.check_throttles(request)
self.renderer, self.accepted_media_type = self.perform_content_negotiation(request) self.renderer, self.accepted_media_type = self.perform_content_negotiation(request)