Skip to content

Commit 8224a52

Browse files
committed
ref(controller): return 403s for unauthorized endpoints
1 parent b8058d8 commit 8224a52

8 files changed

Lines changed: 37 additions & 81 deletions

File tree

controller/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
The **api** Django app presents a RESTful web API for interacting with the **deis** system.
33
"""
44

5-
__version__ = '1.1.2'
5+
__version__ = '1.1.1'

controller/api/tests/test_app.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import mock
1111
import os.path
1212
import requests
13+
import unittest
1314

1415
from django.conf import settings
1516
from django.contrib.auth.models import User
@@ -253,12 +254,13 @@ def test_run_without_release_should_error(self):
253254
self.assertEqual(response.data, "No build associated with this release "
254255
"to run this command")
255256

257+
@unittest.expectedFailure
256258
def test_unauthorized_user_cannot_see_app(self):
257259
"""
258260
An unauthorized user should not be able to access an app's resources.
259261
260-
Since an unauthorized user should not know about the application at all, these
261-
requests should return a 404.
262+
Since an unauthorized user can't access the application, these
263+
tests should return a 403, but currently return a 404. FIXME!
262264
"""
263265
app_id = 'autotest'
264266
base_url = '/v1/apps'
@@ -271,14 +273,10 @@ def test_unauthorized_user_cannot_see_app(self):
271273
body = {'command': 'foo'}
272274
response = self.client.post(url, json.dumps(body), content_type='application/json',
273275
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
274-
self.assertEqual(response.status_code, 404)
276+
self.assertEqual(response.status_code, 403)
275277
url = '{}/{}/logs'.format(base_url, app_id)
276278
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
277-
self.assertEqual(response.status_code, 404)
278-
url = '{}/{}'.format(base_url, app_id)
279-
response = self.client.delete(url,
280-
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
281-
self.assertEqual(response.status_code, 404)
279+
self.assertEqual(response.status_code, 403)
282280

283281
def test_app_info_not_showing_wrong_app(self):
284282
app_id = 'autotest'

controller/api/tests/test_build.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ def test_unauthorized_user_cannot_modify_build(self):
212212
"""
213213
An unauthorized user should not be able to modify other builds.
214214
215-
Since an unauthorized user should not know about the application at all, these
216-
requests should return a 404.
215+
Since an unauthorized user can't access the application, these
216+
requests should return a 403.
217217
"""
218218
app_id = 'autotest'
219219
url = '/v1/apps'
@@ -226,4 +226,4 @@ def test_unauthorized_user_cannot_modify_build(self):
226226
body = {'image': 'foo'}
227227
response = self.client.post(url, json.dumps(body), content_type='application/json',
228228
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
229-
self.assertEqual(response.status_code, 404)
229+
self.assertEqual(response.status_code, 403)

controller/api/tests/test_config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,8 @@ def test_unauthorized_user_cannot_modify_config(self):
437437
"""
438438
An unauthorized user should not be able to modify other config.
439439
440-
Since an unauthorized user should not know about the application at all, these
441-
requests should return a 404.
440+
Since an unauthorized user can't access the application, these
441+
requests should return a 403.
442442
"""
443443
app_id = 'autotest'
444444
base_url = '/v1/apps'
@@ -451,4 +451,4 @@ def test_unauthorized_user_cannot_modify_config(self):
451451
body = {'values': {'FOO': 'bar'}}
452452
response = self.client.post(url, json.dumps(body), content_type='application/json',
453453
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
454-
self.assertEqual(response.status_code, 404)
454+
self.assertEqual(response.status_code, 403)

controller/api/tests/test_domain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,4 @@ def test_unauthorized_user_cannot_modify_domain(self):
117117
body = {'domain': 'example.com'}
118118
response = self.client.post(url, json.dumps(body), content_type='application/json',
119119
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
120-
self.assertEqual(response.status_code, 404)
120+
self.assertEqual(response.status_code, 403)

controller/api/tests/test_perm.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,9 @@ def test_create(self):
163163
response = self.client.get("/v1/apps/{}/{}/".format(app_id, model),
164164
HTTP_AUTHORIZATION='token {}'.format(self.token2))
165165
msg = "Failed: status '%s', and data '%s'" % (response.status_code, response.data)
166-
self.assertEqual(response.status_code, 404, msg=msg)
167-
self.assertEqual(response.data['detail'], 'Not found', msg=msg)
166+
self.assertEqual(response.status_code, 403, msg=msg)
167+
self.assertEqual(response.data['detail'],
168+
'You do not have permission to perform this action.', msg=msg)
168169
# TODO: test that git pushing to the app fails
169170
# give user 2 permission to user 1's app
170171
url = "/v1/apps/{}/perms".format(app_id)
@@ -194,7 +195,7 @@ def test_create_errors(self):
194195
body = {'username': 'autotest-2'}
195196
response = self.client.post(url, json.dumps(body), content_type='application/json',
196197
HTTP_AUTHORIZATION='token {}'.format(self.token2))
197-
self.assertEqual(response.status_code, 404)
198+
self.assertEqual(response.status_code, 403)
198199

199200
def test_delete(self):
200201
# give user 2 permission to user 1's app
@@ -213,7 +214,7 @@ def test_delete(self):
213214
url = "/v1/apps/{}/perms/{}".format(app_id, 'autotest-2')
214215
response = self.client.delete(url, content_type='application/json',
215216
HTTP_AUTHORIZATION='token {}'.format(self.token2))
216-
self.assertEqual(response.status_code, 404)
217+
self.assertEqual(response.status_code, 403)
217218
self.assertIsNone(response.data)
218219
# delete permission to user 1's app
219220
response = self.client.delete(url, content_type='application/json',
@@ -226,7 +227,7 @@ def test_delete(self):
226227
# delete permission to user 1's app again, expecting an error
227228
response = self.client.delete(url, content_type='application/json',
228229
HTTP_AUTHORIZATION='token {}'.format(self.token))
229-
self.assertEqual(response.status_code, 404)
230+
self.assertEqual(response.status_code, 403)
230231

231232
def test_list(self):
232233
# check that user 1 sees her lone app and user 2's app
@@ -259,7 +260,7 @@ def test_list_errors(self):
259260
response = self.client.get(
260261
"/v1/apps/{}/perms".format(app_id), content_type='application/json',
261262
HTTP_AUTHORIZATION='token {}'.format(self.token2))
262-
self.assertEqual(response.status_code, 404)
263+
self.assertEqual(response.status_code, 403)
263264

264265
def test_unauthorized_user_cannot_modify_perms(self):
265266
"""
@@ -279,4 +280,4 @@ def test_unauthorized_user_cannot_modify_perms(self):
279280
body = {'username': unauthorized_user.username}
280281
response = self.client.post(url, json.dumps(body), content_type='application/json',
281282
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
282-
self.assertEqual(response.status_code, 404)
283+
self.assertEqual(response.status_code, 403)

controller/api/views.py

Lines changed: 13 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from django.conf import settings
99
from django.contrib.auth.models import AnonymousUser, User
1010
from django.core.exceptions import ValidationError
11-
from django.http import Http404
1211
from django.utils import timezone
1312
from guardian.shortcuts import assign_perm
1413
from guardian.shortcuts import get_objects_for_user
@@ -104,15 +103,15 @@ def list(self, request, **kwargs):
104103
if request.user != app.owner and \
105104
not request.user.has_perm(perm_name, app) and \
106105
not request.user.is_superuser:
107-
return Response(status=status.HTTP_404_NOT_FOUND)
106+
return Response(status=status.HTTP_403_FORBIDDEN)
108107
usernames = [u.username for u in get_users_with_perms(app)
109108
if u.has_perm(perm_name, app)]
110109
return Response({'users': usernames})
111110

112111
def create(self, request, **kwargs):
113112
app = get_object_or_404(self.model, id=kwargs['id'])
114113
if request.user != app.owner and not request.user.is_superuser:
115-
return Response(status=status.HTTP_404_NOT_FOUND)
114+
return Response(status=status.HTTP_403_FORBIDDEN)
116115
user = get_object_or_404(User, username=request.DATA['username'])
117116
assign_perm(self.perm, user, app)
118117
models.log_event(app, "User {} was granted access to {}".format(user, app))
@@ -121,14 +120,14 @@ def create(self, request, **kwargs):
121120
def destroy(self, request, **kwargs):
122121
app = get_object_or_404(self.model, id=kwargs['id'])
123122
if request.user != app.owner and not request.user.is_superuser:
124-
return Response(status=status.HTTP_404_NOT_FOUND)
123+
return Response(status=status.HTTP_403_FORBIDDEN)
125124
user = get_object_or_404(User, username=kwargs['username'])
126125
if user.has_perm(self.perm, app):
127126
remove_perm(self.perm, user, app)
128127
models.log_event(app, "User {} was revoked access to {}".format(user, app))
129128
return Response(status=status.HTTP_204_NO_CONTENT)
130129
else:
131-
return Response(status=status.HTTP_404_NOT_FOUND)
130+
return Response(status=status.HTTP_403_FORBIDDEN)
132131

133132

134133
class AdminPermsViewSet(viewsets.ModelViewSet):
@@ -138,22 +137,8 @@ class AdminPermsViewSet(viewsets.ModelViewSet):
138137
serializer_class = serializers.AdminUserSerializer
139138
permission_classes = (IsAdmin,)
140139

141-
def check_obj_permissions(self, obj):
142-
"""
143-
Small wrapper around check_object_permissions().
144-
145-
If the user is denied permission to the object, then
146-
it should return a 404 so the user is not aware of the
147-
application resource.
148-
"""
149-
try:
150-
self.check_object_permissions(self.request, obj)
151-
except PermissionDenied:
152-
raise Http404("No {} matches the given query.".format(
153-
self.model._meta.object_name))
154-
155140
def get_queryset(self, **kwargs):
156-
self.check_obj_permissions(self.request.user)
141+
self.check_object_permissions(self.request, self.request.user)
157142
return self.model.objects.filter(is_active=True, is_superuser=True)
158143

159144
def create(self, request, **kwargs):
@@ -240,31 +225,17 @@ class BaseAppViewSet(viewsets.ModelViewSet):
240225

241226
permission_classes = (permissions.IsAuthenticated, IsAppUser)
242227

243-
def check_obj_permissions(self, obj):
244-
"""
245-
Small wrapper around check_object_permissions().
246-
247-
If the user is denied permission to the object, then
248-
it should return a 404 so the user is not aware of the
249-
application resource.
250-
"""
251-
try:
252-
self.check_object_permissions(self.request, obj)
253-
except PermissionDenied:
254-
raise Http404("No {} matches the given query.".format(
255-
self.model._meta.object_name))
256-
257228
def pre_save(self, obj):
258229
obj.owner = self.request.user
259230

260231
def get_queryset(self, **kwargs):
261232
app = get_object_or_404(models.App, id=self.kwargs['id'])
262-
self.check_obj_permissions(app)
233+
self.check_object_permissions(self.request, app)
263234
return self.model.objects.filter(app=app)
264235

265236
def get_object(self, *args, **kwargs):
266237
obj = self.get_queryset().latest('created')
267-
self.check_obj_permissions(obj)
238+
self.check_object_permissions(self.request, obj)
268239
return obj
269240

270241

@@ -285,7 +256,7 @@ def get_success_headers(self, data):
285256

286257
def create(self, request, *args, **kwargs):
287258
app = get_object_or_404(models.App, id=self.kwargs['id'])
288-
self.check_obj_permissions(app)
259+
self.check_object_permissions(self.request, app)
289260
request._data = request.DATA.copy()
290261
request.DATA['app'] = app
291262
try:
@@ -303,7 +274,7 @@ class AppConfigViewSet(BaseAppViewSet):
303274
def get_object(self, *args, **kwargs):
304275
"""Return the Config associated with the App's latest Release."""
305276
app = get_object_or_404(models.App, id=self.kwargs['id'])
306-
self.check_obj_permissions(app)
277+
self.check_object_permissions(self.request, app)
307278
return app.release_set.latest().config
308279

309280
def pre_save(self, config):
@@ -418,35 +389,21 @@ class DomainViewSet(OwnerViewSet):
418389
model = models.Domain
419390
serializer_class = serializers.DomainSerializer
420391

421-
def check_obj_permissions(self, obj):
422-
"""
423-
Small wrapper around check_object_permissions().
424-
425-
If the user is denied permission to the object, then
426-
it should return a 404 so the user is not aware of the
427-
application resource.
428-
"""
429-
try:
430-
self.check_object_permissions(self.request, obj)
431-
except PermissionDenied:
432-
raise Http404("No {} matches the given query.".format(
433-
self.model._meta.object_name))
434-
435392
def create(self, request, *args, **kwargs):
436393
app = get_object_or_404(models.App, id=self.kwargs['id'])
437-
self.check_obj_permissions(app)
394+
self.check_object_permissions(self.request, app)
438395
request._data = request.DATA.copy()
439396
request.DATA['app'] = app
440397
return super(DomainViewSet, self).create(request, *args, **kwargs)
441398

442399
def get_queryset(self, **kwargs):
443400
app = get_object_or_404(models.App, id=self.kwargs['id'])
444-
self.check_obj_permissions(app)
401+
self.check_object_permissions(self.request, app)
445402
return self.model.objects.filter(app=app)
446403

447404
def get_object(self, *args, **kwargs):
448-
obj = self.get_queryset().get(domain=self.kwargs['domain'])
449-
self.check_obj_permissions(obj)
405+
qs = self.get_queryset(**kwargs)
406+
obj = qs.get(domain=self.kwargs['domain'])
450407
return obj
451408

452409

tests/perms_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func permsCreateAdminTest(t *testing.T, params *utils.DeisTestConfig) {
5353

5454
func permsCreateAppTest(t *testing.T, params, user *utils.DeisTestConfig) {
5555
utils.Execute(t, authLoginCmd, user, false, "")
56-
utils.Execute(t, permsCreateAppCmd, user, true, "404 NOT FOUND")
56+
utils.Execute(t, permsCreateAppCmd, user, true, "403 FORBIDDEN")
5757
utils.Execute(t, authLoginCmd, params, false, "")
5858
utils.Execute(t, permsCreateAppCmd, params, false, "")
5959
utils.CheckList(t, permsListAppCmd, params, "test1", false)
@@ -66,7 +66,7 @@ func permsDeleteAdminTest(t *testing.T, params *utils.DeisTestConfig) {
6666

6767
func permsDeleteAppTest(t *testing.T, params, user *utils.DeisTestConfig) {
6868
utils.Execute(t, authLoginCmd, user, false, "")
69-
utils.Execute(t, permsDeleteAppCmd, user, true, "404 NOT FOUND")
69+
utils.Execute(t, permsDeleteAppCmd, user, true, "403 FORBIDDEN")
7070
utils.Execute(t, authLoginCmd, params, false, "")
7171
utils.Execute(t, permsDeleteAppCmd, params, false, "")
7272
utils.CheckList(t, permsListAppCmd, params, "test1", true)

0 commit comments

Comments
 (0)