Skip to content

Commit a0ca70c

Browse files
author
Matthew Fisher
committed
Merge pull request #1736 from bacongobbler/admin-as-a-god
test(controller): verify that admins are gods
2 parents 75ba179 + 8113664 commit a0ca70c

11 files changed

Lines changed: 333 additions & 120 deletions

File tree

controller/api/fixtures/test_sharing.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"first_name": "",
2626
"last_name": "",
2727
"is_active": true,
28-
"is_superuser": false,
29-
"is_staff": false,
28+
"is_superuser": true,
29+
"is_staff": true,
3030
"last_login": "2013-11-25T21:58:47.420Z",
3131
"groups": [],
3232
"user_permissions": [],
@@ -77,5 +77,16 @@
7777
"owner": 2,
7878
"id": "autotest-1-app"
7979
}
80+
},
81+
{
82+
"pk": "5a09a1e0-a27e-4839-928b-449310ed90f1",
83+
"model": "api.app",
84+
"fields": {
85+
"updated": "2013-11-25T22:09:36.726Z",
86+
"created": "2013-11-25T22:09:36.726Z",
87+
"cluster": "80e9db5a-d394-41c7-b153-3ea0314dc8ca",
88+
"owner": 3,
89+
"id": "autotest-2-app"
90+
}
8091
}
8192
]

controller/api/permissions.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from rest_framework import permissions
2+
from django.conf import settings
3+
from django.contrib.auth.models import AnonymousUser
4+
5+
from api import models
6+
7+
8+
class IsAnonymous(permissions.BasePermission):
9+
"""
10+
View permission to allow anonymous users.
11+
"""
12+
13+
def has_permission(self, request, view):
14+
"""
15+
Return `True` if permission is granted, `False` otherwise.
16+
"""
17+
return type(request.user) is AnonymousUser
18+
19+
20+
class IsOwner(permissions.BasePermission):
21+
"""
22+
Object-level permission to allow only owners of an object to access it.
23+
Assumes the model instance has an `owner` attribute.
24+
"""
25+
26+
def has_object_permission(self, request, view, obj):
27+
if hasattr(obj, 'owner'):
28+
return obj.owner == request.user
29+
else:
30+
return False
31+
32+
33+
class IsAppUser(permissions.BasePermission):
34+
"""
35+
Object-level permission to allow owners or collaborators to access
36+
an app-related model.
37+
"""
38+
def has_object_permission(self, request, view, obj):
39+
if isinstance(obj, models.App) and obj.owner == request.user:
40+
return True
41+
elif hasattr(obj, 'app') and obj.app.owner == request.user:
42+
return True
43+
elif request.user.has_perm('use_app', obj):
44+
return request.method != 'DELETE'
45+
elif hasattr(obj, 'app') and request.user.has_perm('use_app', obj.app):
46+
return request.method != 'DELETE'
47+
else:
48+
return False
49+
50+
51+
class IsAdmin(permissions.BasePermission):
52+
"""
53+
View permission to allow only admins.
54+
"""
55+
56+
def has_permission(self, request, view):
57+
"""
58+
Return `True` if permission is granted, `False` otherwise.
59+
"""
60+
return request.user.is_superuser
61+
62+
63+
class IsAdminOrSafeMethod(permissions.BasePermission):
64+
"""
65+
View permission to allow only admins to use unsafe methods
66+
including POST, PUT, DELETE.
67+
68+
This allows
69+
"""
70+
71+
def has_permission(self, request, view):
72+
"""
73+
Return `True` if permission is granted, `False` otherwise.
74+
"""
75+
return request.method in permissions.SAFE_METHODS or request.user.is_superuser
76+
77+
78+
class HasRegistrationAuth(permissions.BasePermission):
79+
"""
80+
Checks to see if registration is enabled
81+
"""
82+
def has_permission(self, request, view):
83+
return settings.REGISTRATION_ENABLED
84+
85+
86+
class HasBuilderAuth(permissions.BasePermission):
87+
"""
88+
View permission to allow builder to perform actions
89+
with a special HTTP header
90+
"""
91+
92+
def has_permission(self, request, view):
93+
"""
94+
Return `True` if permission is granted, `False` otherwise.
95+
"""
96+
auth_header = request.environ.get('HTTP_X_DEIS_BUILDER_AUTH')
97+
if not auth_header:
98+
return False
99+
return auth_header == settings.BUILDER_KEY

controller/api/tests/test_app.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def test_app_actions(self):
9090
response = self.client.post(url)
9191
self.assertEqual(response.status_code, 200)
9292
self.assertEqual(response.data, FAKE_LOG_DATA)
93+
os.remove(path)
9394
# test run
9495
url = '/api/apps/{app_id}/run'.format(**locals())
9596
body = {'command': 'ls -al'}
@@ -136,6 +137,55 @@ def test_app_errors(self):
136137
response = self.client.get(url)
137138
self.assertEquals(response.status_code, 404)
138139

140+
def test_admin_can_manage_other_apps(self):
141+
"""Administrators of Deis should be able to manage all applications.
142+
"""
143+
# log in as non-admin user and create an app
144+
self.assertTrue(
145+
self.client.login(username='autotest2', password='password'))
146+
app_id = 'autotest'
147+
url = '/api/apps'
148+
body = {'cluster': 'autotest', 'id': app_id}
149+
response = self.client.post(url, json.dumps(body), content_type='application/json')
150+
# log in as admin, check to see if they have access
151+
self.assertTrue(
152+
self.client.login(username='autotest', password='password'))
153+
url = '/api/apps/{}'.format(app_id)
154+
response = self.client.get(url)
155+
self.assertEqual(response.status_code, 200)
156+
# check app logs
157+
url = '/api/apps/{app_id}/logs'.format(**locals())
158+
response = self.client.post(url)
159+
self.assertEqual(response.status_code, 200)
160+
self.assertIn('autotest2 created initial release', response.data)
161+
# run one-off commands
162+
url = '/api/apps/{app_id}/run'.format(**locals())
163+
body = {'command': 'ls -al'}
164+
response = self.client.post(url, json.dumps(body), content_type='application/json')
165+
self.assertEqual(response.status_code, 200)
166+
self.assertEqual(response.data[0], 0)
167+
# delete the app
168+
url = '/api/apps/{}'.format(app_id)
169+
response = self.client.delete(url)
170+
self.assertEqual(response.status_code, 204)
171+
172+
def test_admin_can_see_other_apps(self):
173+
"""If a user creates an application, the administrator should be able
174+
to see it.
175+
"""
176+
# log in as non-admin user and create an app
177+
self.assertTrue(
178+
self.client.login(username='autotest2', password='password'))
179+
app_id = 'autotest'
180+
url = '/api/apps'
181+
body = {'cluster': 'autotest', 'id': app_id}
182+
response = self.client.post(url, json.dumps(body), content_type='application/json')
183+
# log in as admin
184+
self.assertTrue(
185+
self.client.login(username='autotest', password='password'))
186+
response = self.client.get(url)
187+
self.assertEqual(response.data['count'], 1)
188+
139189

140190
FAKE_LOG_DATA = """
141191
2013-08-15 12:41:25 [33454] [INFO] Starting gunicorn 17.5

controller/api/tests/test_build.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,25 @@ def test_build_str(self):
179179
build = Build.objects.get(uuid=response.data['uuid'])
180180
self.assertEqual(str(build), "{}-{}".format(
181181
response.data['app'], response.data['uuid'][:7]))
182+
183+
@mock.patch('requests.post', mock_import_repository_task)
184+
def test_admin_can_create_builds_on_other_apps(self):
185+
"""If a user creates an application, an administrator should be able
186+
to push builds.
187+
"""
188+
# create app as non-admin
189+
self.client.login(username='autotest2', password='password')
190+
url = '/api/apps'
191+
body = {'cluster': 'autotest'}
192+
response = self.client.post(url, json.dumps(body), content_type='application/json')
193+
self.assertEqual(response.status_code, 201)
194+
app_id = response.data['id']
195+
# post a new build as admin
196+
self.client.login(username='autotest', password='password')
197+
url = "/api/apps/{app_id}/builds".format(**locals())
198+
body = {'image': 'autotest/example'}
199+
response = self.client.post(url, json.dumps(body), content_type='application/json')
200+
self.assertEqual(response.status_code, 201)
201+
build = Build.objects.get(uuid=response.data['uuid'])
202+
self.assertEqual(str(build), "{}-{}".format(
203+
response.data['app'], response.data['uuid'][:7]))

controller/api/tests/test_config.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,25 @@ def test_config_str(self):
134134
config = Config.objects.get(uuid=config5['uuid'])
135135
self.assertEqual(str(config), "{}-{}".format(config5['app'], config5['uuid'][:7]))
136136

137+
@mock.patch('requests.post', mock_import_repository_task)
138+
def test_admin_can_create_config_on_other_apps(self):
139+
"""If a non-admin creates an app, an administrator should be able to set config
140+
values for that app.
141+
"""
142+
self.client.login(username='autotest2', password='password')
143+
url = '/api/apps'
144+
body = {'cluster': 'autotest'}
145+
response = self.client.post(url, json.dumps(body), content_type='application/json')
146+
self.assertEqual(response.status_code, 201)
147+
app_id = response.data['id']
148+
self.client.login(username='autotest', password='password')
149+
url = "/api/apps/{app_id}/config".format(**locals())
150+
# set an initial config value
151+
body = {'values': json.dumps({'PORT': '5000'})}
152+
response = self.client.post(url, json.dumps(body), content_type='application/json')
153+
self.assertEqual(response.status_code, 201)
154+
self.assertIn('PORT', json.loads(response.data['values']))
155+
137156
@mock.patch('requests.post', mock_import_repository_task)
138157
def test_limit_memory(self):
139158
"""
@@ -348,4 +367,3 @@ def test_tags(self):
348367
self.assertEqual(self.client.put(url).status_code, 405)
349368
self.assertEqual(self.client.patch(url).status_code, 405)
350369
self.assertEqual(self.client.delete(url).status_code, 405)
351-
return tags4

controller/api/tests/test_container.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,27 @@ def test_container_scale_errors(self):
400400
body = {'web': 1}
401401
response = self.client.post(url, json.dumps(body), content_type='application/json')
402402
self.assertEqual(response.status_code, 204)
403+
404+
def test_admin_can_manage_other_containers(self):
405+
"""If a non-admin user creates a container, an administrator should be able to
406+
manage it.
407+
"""
408+
self.client.login(username='autotest2', password='password')
409+
url = '/api/apps'
410+
body = {'cluster': 'autotest'}
411+
response = self.client.post(url, json.dumps(body), content_type='application/json')
412+
self.assertEqual(response.status_code, 201)
413+
app_id = response.data['id']
414+
# post a new build
415+
url = "/api/apps/{app_id}/builds".format(**locals())
416+
body = {'image': 'autotest/example', 'sha': 'a'*40,
417+
'procfile': json.dumps({'web': 'node server.js', 'worker': 'node worker.js'})}
418+
response = self.client.post(url, json.dumps(body), content_type='application/json')
419+
self.assertEqual(response.status_code, 201)
420+
# login as admin
421+
self.client.login(username='autotest', password='password')
422+
# scale up
423+
url = "/api/apps/{app_id}/scale".format(**locals())
424+
body = {'web': 4, 'worker': 2}
425+
response = self.client.post(url, json.dumps(body), content_type='application/json')
426+
self.assertEqual(response.status_code, 204)

controller/api/tests/test_domain.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,23 @@ def test_manage_domain_invalid_domain(self):
8989
self.assertEqual(response.status_code, 400)
9090

9191
def test_manage_domain_wildcard(self):
92-
# Wildcards are not allowed for now.
92+
"""Wildcards are not allowed for now."""
9393
url = '/api/apps/{app_id}/domains'.format(app_id=self.app_id)
9494
body = {'domain': '*.deis.example.com'}
9595
response = self.client.post(url, json.dumps(body), content_type='application/json')
9696
self.assertEqual(response.status_code, 400)
97+
98+
def test_admin_can_add_domains_to_other_apps(self):
99+
"""If a non-admin user creates an app, an administrator should be able to add
100+
domains to it.
101+
"""
102+
self.client.login(username='autotest2', password='password')
103+
url = '/api/apps'
104+
body = {'cluster': 'autotest'}
105+
response = self.client.post(url, json.dumps(body), content_type='application/json')
106+
self.assertEqual(response.status_code, 201)
107+
self.client.login(username='autotest', password='password')
108+
url = '/api/apps/{}/domains'.format(self.app_id)
109+
body = {'domain': 'example.deis.example.com'}
110+
response = self.client.post(url, json.dumps(body), content_type='application/json')
111+
self.assertEqual(response.status_code, 201)

controller/api/tests/test_hooks.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,30 @@ def test_config_hook(self):
251251
self.assertEqual(response.status_code, 200)
252252
self.assertIn('values', response.data)
253253
self.assertEqual(values, response.data['values'])
254+
255+
def test_admin_can_hook(self):
256+
"""Administrator should be able to create build hooks on non-admin apps.
257+
"""
258+
"""Test creating a Push via the API"""
259+
self.client.login(username='autotest2', password='password')
260+
url = '/api/apps'
261+
body = {'cluster': 'autotest'}
262+
response = self.client.post(url, json.dumps(body), content_type='application/json')
263+
self.assertEqual(response.status_code, 201)
264+
app_id = response.data['id']
265+
self.client.login(username='autotest', password='password')
266+
# prepare a push body
267+
DOCKERFILE = """
268+
FROM busybox
269+
CMD /bin/true
270+
"""
271+
body = {'receive_user': 'autotest',
272+
'receive_repo': app_id,
273+
'image': '{app_id}:v2'.format(**locals()),
274+
'sha': 'ecdff91c57a0b9ab82e89634df87e293d259a3aa',
275+
'dockerfile': DOCKERFILE}
276+
url = '/api/hooks/builds'
277+
response = self.client.post(url, json.dumps(body), content_type='application/json',
278+
HTTP_X_DEIS_BUILDER_AUTH=settings.BUILDER_KEY)
279+
self.assertEqual(response.status_code, 200)
280+
self.assertEqual(response.data['release']['version'], 2)

0 commit comments

Comments
 (0)