Repository: deis/deis

Issue:
Some API calls return 404 instead of 403 when a user doesn't have permission
The following CLI commands will return 404 instead of 403 (as they should):
- info
- scale
- logs
- run
- destroy

Related: #2854



Discussion:
Note that this isn't necessarily comprehensive - these were noticed in hand-testing various commands as part of #2856.

While running through #2898, I found that this is a limitation that we brought upon ourselves with our queryset and how we model our API endpoints.

In our AppViewSet, we set `.get_queryset()` to give us only the applications that we have either created (i.e. we are the owner) or ones which we were given permission to use (`use_app` from django-guardian). This is such that when we list apps via `/v1/apps`, it will only show us that filtered viewset. However, that also limits the scope on what applications we can act upon. Since `.get_queryset()` returns a queryset of applications that we only know about, it returns a 404 when we try to "ping" an application we were not given access to.

Therefore, there are a few choices:
1. manually patch `.list()` to use unbounded queryset (ugly, but backwards-compatible)
2. change `.get_queryset()` to be unbounded, add a filter to `/v1/apps` to show apps relevant to the user, modify `deis apps:list` in the client to use this new endpoint (backwards-incompatible, but RESTful)
3. close this as a limitation with a user's available queryset.

(1) Would be the quickest solution, but ultimately means that we are manually patching one of our endpoints and cannot take advantage of future DRF fixes to `.list()` without patching it ourselves. (2) would be "the right thing" to do, but we're unable to break endpoint APIs any more now that we're considered backwards-compatible. Older clients would also break in that they'd be displaying all applications on the platform rather than the user's limited queryset of applications.


Resolving patch:
diff --git a/controller/api/views.py b/controller/api/views.py
--- a/controller/api/views.py
+++ b/controller/api/views.py
@@ -107,9 +107,24 @@ class AppViewSet(BaseDeisViewSet):
     model = models.App
     serializer_class = serializers.AppSerializer
 
-    def get_queryset(self, **kwargs):
-        return super(AppViewSet, self).get_queryset(**kwargs) | \
+    def get_queryset(self, *args, **kwargs):
+        return self.model.objects.all(*args, **kwargs)
+
+    def list(self, request, *args, **kwargs):
+        """
+        HACK: Instead of filtering by the queryset, we limit the queryset to list only the apps
+        which are owned by the user as well as any apps they have been given permission to
+        interact with.
+        """
+        queryset = super(AppViewSet, self).get_queryset(**kwargs) | \
             get_objects_for_user(self.request.user, 'api.use_app')
+        instance = self.filter_queryset(queryset)
+        page = self.paginate_queryset(instance)
+        if page is not None:
+            serializer = self.get_pagination_serializer(page)
+        else:
+            serializer = self.get_serializer(instance, many=True)
+        return Response(serializer.data)
 
     def post_save(self, app):
         app.create()


Test patch:
diff --git a/controller/api/tests/test_app.py b/controller/api/tests/test_app.py
--- a/controller/api/tests/test_app.py
+++ b/controller/api/tests/test_app.py
@@ -10,7 +10,6 @@
 import mock
 import os.path
 import requests
-import unittest
 
 from django.conf import settings
 from django.contrib.auth.models import User
@@ -268,7 +267,6 @@ def test_run_without_release_should_error(self):
         self.assertEqual(response.data, "No build associated with this release "
                                         "to run this command")
 
-    @unittest.expectedFailure
     def test_unauthorized_user_cannot_see_app(self):
         """
         An unauthorized user should not be able to access an app's resources.
diff --git a/controller/api/tests/test_container.py b/controller/api/tests/test_container.py
--- a/controller/api/tests/test_container.py
+++ b/controller/api/tests/test_container.py
@@ -9,7 +9,6 @@
 import json
 import mock
 import requests
-import unittest
 
 from django.contrib.auth.models import User
 from django.test import TransactionTestCase
@@ -541,7 +540,6 @@ def test_run_command_good(self):
         rc, output = c.run('echo hi')
         self.assertEqual(json.loads(output)['entrypoint'], '/runner/init')
 
-    @unittest.expectedFailure
     def test_scale_with_unauthorized_user_returns_403(self):
         """An unauthorized user should not be able to access an app's resources.