Skip to content

Commit 3e40eb4

Browse files
dlfoersterauvipy
andauthored
Fix#6855 browsable api extra action (#9934)
* + add documentation for change strategy * Fix browsable API crash when rendering OPTIONS for extra actions --------- Co-authored-by: Asif Saif Uddin {"Auvi":"অভি"} <auvipy@gmail.com>
1 parent d6e11ec commit 3e40eb4

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

rest_framework/renderers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,17 @@ def get_rendered_html_form(self, data, view, method, request):
496496
existing_serializer = None
497497

498498
with override_method(view, request, method) as request:
499+
if method == 'OPTIONS':
500+
# The browsable API only needs a placeholder for OPTIONS, so
501+
# avoid object-level permission checks against serializer.instance.
502+
if method not in view.allowed_methods:
503+
return
504+
try:
505+
view.check_permissions(request)
506+
except exceptions.APIException:
507+
return
508+
return True
509+
499510
if not self.show_form_for_method(view, method, request, instance):
500511
return
501512

tests/test_renderers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,9 +731,34 @@ class AuthExampleViewSet(ExampleViewSet):
731731
class SimpleSerializer(serializers.Serializer):
732732
name = serializers.CharField()
733733

734+
class CrashOnObjectPermission(permissions.BasePermission):
735+
def has_permission(self, request, view):
736+
return True
737+
738+
def has_object_permission(self, request, view, obj):
739+
return obj.user.is_staff
740+
741+
class Issue6855Serializer(serializers.Serializer):
742+
name = serializers.CharField()
743+
744+
class Issue6855Object:
745+
def __init__(self, name):
746+
self.name = name
747+
748+
class Issue6855ViewSet(ViewSet):
749+
@action(detail=True)
750+
def extra_action(self, request, pk=None):
751+
serializer = BrowsableAPIRendererTests.Issue6855Serializer(
752+
BrowsableAPIRendererTests.Issue6855Object(name='demo')
753+
)
754+
return Response(serializer.data)
755+
756+
Issue6855ViewSet.permission_classes = [CrashOnObjectPermission]
757+
734758
router = SimpleRouter()
735759
router.register('examples', ExampleViewSet, basename='example')
736760
router.register('auth-examples', AuthExampleViewSet, basename='auth-example')
761+
router.register('issue-6855', Issue6855ViewSet, basename='issue-6855')
737762
urlpatterns = [path('api/', include(router.urls))]
738763

739764
def setUp(self):
@@ -820,6 +845,34 @@ def test_extra_actions_dropdown_not_authed(self):
820845
assert '/api/examples/list_action/' not in resp.content.decode()
821846
assert '>Extra list action<' not in resp.content.decode()
822847

848+
def test_options_form_does_not_check_object_permissions_for_extra_action(self):
849+
resp = self.client.get('/api/issue-6855/1/extra_action/', HTTP_ACCEPT='text/html')
850+
assert resp.status_code == status.HTTP_200_OK
851+
852+
def test_delete_form_still_checks_object_permissions(self):
853+
class ObjectPermissionDenied(permissions.BasePermission):
854+
def has_permission(self, request, view):
855+
return True
856+
857+
def has_object_permission(self, request, view, obj):
858+
return False
859+
860+
class DummyObject:
861+
name = 'Name'
862+
863+
class DummyDeleteView(APIView):
864+
permission_classes = [ObjectPermissionDenied]
865+
866+
def delete(self, request):
867+
return Response()
868+
869+
request = Request(APIRequestFactory().get('/'))
870+
serializer = BrowsableAPIRendererTests.SimpleSerializer(instance=DummyObject())
871+
delete_form = self.renderer.get_rendered_html_form(
872+
serializer.data, DummyDeleteView(), 'DELETE', request
873+
)
874+
assert delete_form is None
875+
823876

824877
class AdminRendererTests(TestCase):
825878

0 commit comments

Comments
 (0)