Repository: encode/django-rest-framework

Issue:
List of actions of a viewset are visible from unauthenticated users in browsable api (latest 3.9.4)
## Checklist

- [ ] I have verified that that issue exists against the `master` branch of Django REST framework.
- [x] I have searched for similar issues in both open and closed tickets and cannot find a duplicate.
- [x] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.)
- [x] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.)
- [x] I have reduced the issue to the simplest possible case.
- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.)

## Steps to reproduce
Create a model viewset with different actions (detail=True or False), make sure the viewset and actions require users to be authenticated, and get the following response on list and retrieve views and actions: HTTP 401 Unauthorized "detail": "Authentication credentials were not provided."

## Expected behavior
Unauthenticated users should not have access to the list of actions (on list view and on retrieve view) in the "Extra actions" dropdown if they don't have access to the views and actions.

## Actual behavior
In browsable api of latest DRF (3.9.4) unauthenticated users can see the list of detail=False actions on the list view and the list of detail=True actions on the retrieve view, even though they don't have access to retrieve/list/actions views (401 unauthorized) 


Discussion:

Resolving patch:
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -604,10 +604,13 @@ def get_description(self, view, status_code):
     def get_breadcrumbs(self, request):
         return get_breadcrumbs(request.path, request)
 
-    def get_extra_actions(self, view):
-        if hasattr(view, 'get_extra_action_url_map'):
-            return view.get_extra_action_url_map()
-        return None
+    def get_extra_actions(self, view, status_code):
+        if (status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)):
+            return None
+        elif not hasattr(view, 'get_extra_action_url_map'):
+            return None
+
+        return view.get_extra_action_url_map()
 
     def get_filter_form(self, data, view, request):
         if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'):
@@ -695,7 +698,7 @@ def get_context(self, data, accepted_media_type, renderer_context):
             'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
             'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
 
-            'extra_actions': self.get_extra_actions(view),
+            'extra_actions': self.get_extra_actions(view, response.status_code),
 
             'filter_form': self.get_filter_form(data, view, request),
 


Test patch:
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -631,8 +631,12 @@ def list(self, request):
         def list_action(self, request):
             raise NotImplementedError
 
+    class AuthExampleViewSet(ExampleViewSet):
+        permission_classes = [permissions.IsAuthenticated]
+
     router = SimpleRouter()
     router.register('examples', ExampleViewSet, basename='example')
+    router.register('auth-examples', AuthExampleViewSet, basename='auth-example')
     urlpatterns = [url(r'^api/', include(router.urls))]
 
     def setUp(self):
@@ -657,6 +661,12 @@ def test_extra_actions_dropdown(self):
         assert '/api/examples/list_action/' in resp.content.decode()
         assert '>Extra list action<' in resp.content.decode()
 
+    def test_extra_actions_dropdown_not_authed(self):
+        resp = self.client.get('/api/unauth-examples/', HTTP_ACCEPT='text/html')
+        assert 'id="extra-actions-menu"' not in resp.content.decode()
+        assert '/api/examples/list_action/' not in resp.content.decode()
+        assert '>Extra list action<' not in resp.content.decode()
+
 
 class AdminRendererTests(TestCase):