Merge lp:~joetalbott/uci-engine/user_auth into lp:uci-engine

Proposed by Joe Talbott
Status: Needs review
Proposed branch: lp:~joetalbott/uci-engine/user_auth
Merge into: lp:uci-engine
Diff against target: 573 lines (+336/-17)
11 files modified
charms/precise/wsgi-app/config.yaml (+5/-0)
charms/precise/wsgi-app/hooks/hooks.py (+35/-0)
charms/precise/wsgi-app/unit_tests/test_hooks.py (+10/-0)
ci-utils/ci_utils/tastypie/test.py (+29/-7)
ticket_system/ticket/api.py (+10/-8)
ticket_system/ticket/authorization.py (+97/-0)
ticket_system/ticket/models.py (+3/-0)
ticket_system/ticket/tests/__init__.py (+1/-0)
ticket_system/ticket/tests/test_authorization.py (+138/-0)
ticket_system/ticket/tests/test_write_api.py (+1/-2)
ticket_system/ticket_system/settings.py (+7/-0)
To merge this branch: bzr merge lp:~joetalbott/uci-engine/user_auth
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Celso Providelo (community) Needs Fixing
Paul Larson Approve
Review via email: mp+239067@code.launchpad.net

Commit message

ticket-system - Add first pass custom authorization class for APIs.

Description of the change

ticket-system - Add first pass custom authorization class for APIs.

To post a comment you must log in.
Revision history for this message
Paul Larson (pwlars) wrote :

minor comment, otherwise looks like a good start to me

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:833
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1612/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1612/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Celso Providelo (cprov) wrote :

I have some questions inline that needs clarification, I am probably misunderstanding things.

review: Needs Information
lp:~joetalbott/uci-engine/user_auth updated
834. By Joe Talbott

ticket-system - Add authentication tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:834
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1613/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1613/rebuild

review: Approve (continuous-integration)
lp:~joetalbott/uci-engine/user_auth updated
835. By Joe Talbott

ticket-system - Switch away from global permissions.

836. By Joe Talbott

ticket-system - Turn off debugging.

837. By Joe Talbott

ticket-system - Fix typo that snuck into previous commit.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:837
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1618/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1618/rebuild

review: Needs Fixing (continuous-integration)
lp:~joetalbott/uci-engine/user_auth updated
838. By Joe Talbott

ticket-system - remove obsolete global permission code.

839. By Joe Talbott

ticket-system - Add docstring explaining custom authorization class.

840. By Joe Talbott

ticket-system - Add config option for internal hosts.

* adds tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:840
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1623/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1623/rebuild

review: Needs Fixing (continuous-integration)
lp:~joetalbott/uci-engine/user_auth updated
841. By Joe Talbott

ci-utils - Pep8 fixes for tastypie tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:841
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1624/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1624/rebuild

review: Approve (continuous-integration)
Revision history for this message
Celso Providelo (cprov) wrote :

Joe,

It looks pretty good, I just have few inline comments to be addresses/discussed about testing infrastructure.

review: Needs Fixing
lp:~joetalbott/uci-engine/user_auth updated
842. By Joe Talbott

ticket-system - Clean up tests and remove oauth bits.

843. By Joe Talbott

merge with trunk

* resolve conflicts

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:843
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1679/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1679/rebuild

review: Needs Fixing (continuous-integration)
lp:~joetalbott/uci-engine/user_auth updated
844. By Joe Talbott

Remove tests for unneeded functionality

We won't be bulk adding or updating reviews via the /review/ end-point so drop
the tests. This was a side-effect of finding out that we need to allow PUT for
these tests to pass but allowing PUT makes POST'ing a list not add entries if a
matching one already exists, thus causing another more important test to fail.

845. By Joe Talbott

Remove no longer needed self.review_permission

846. By Joe Talbott

Re-add overzealously removed import.

847. By Joe Talbott

Use .count() on querysets not len()

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:847
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1680/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1680/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Celso Providelo (cprov) wrote :

Thanks for the changes, Joe.

We are very close! Please address my inline comments and the test failure.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:847
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1684/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1684/rebuild

review: Approve (continuous-integration)
lp:~joetalbott/uci-engine/user_auth updated
848. By Joe Talbott

Use TicketTastypieTestCase and ticket.uuid rather than .pk.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:848
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1686/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1686/rebuild

review: Needs Fixing (continuous-integration)
lp:~joetalbott/uci-engine/user_auth updated
849. By Joe Talbott

ticket-system - flake8 cleanup.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:849
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1689/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1689/rebuild

review: Approve (continuous-integration)

Unmerged revisions

849. By Joe Talbott

ticket-system - flake8 cleanup.

848. By Joe Talbott

Use TicketTastypieTestCase and ticket.uuid rather than .pk.

847. By Joe Talbott

Use .count() on querysets not len()

846. By Joe Talbott

Re-add overzealously removed import.

845. By Joe Talbott

Remove no longer needed self.review_permission

844. By Joe Talbott

Remove tests for unneeded functionality

We won't be bulk adding or updating reviews via the /review/ end-point so drop
the tests. This was a side-effect of finding out that we need to allow PUT for
these tests to pass but allowing PUT makes POST'ing a list not add entries if a
matching one already exists, thus causing another more important test to fail.

843. By Joe Talbott

merge with trunk

* resolve conflicts

842. By Joe Talbott

ticket-system - Clean up tests and remove oauth bits.

841. By Joe Talbott

ci-utils - Pep8 fixes for tastypie tests

840. By Joe Talbott

ticket-system - Add config option for internal hosts.

* adds tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charms/precise/wsgi-app/config.yaml'
2--- charms/precise/wsgi-app/config.yaml 2014-10-15 10:51:10 +0000
3+++ charms/precise/wsgi-app/config.yaml 2014-11-06 19:15:32 +0000
4@@ -63,6 +63,11 @@
5 default: "INFO"
6 description: The logging level.
7
8+ internal_hosts:
9+ type: string
10+ default: ''
11+ description: A comma separated list of internal hosts that don't need permissions checked. Ticket-system only.
12+
13 # Apt info used by charmhelpers
14 install_sources:
15 type: string
16
17=== modified file 'charms/precise/wsgi-app/hooks/hooks.py'
18--- charms/precise/wsgi-app/hooks/hooks.py 2014-10-16 13:24:26 +0000
19+++ charms/precise/wsgi-app/hooks/hooks.py 2014-11-06 19:15:32 +0000
20@@ -244,6 +244,30 @@
21 return reverseproxies
22
23
24+def _get_internal_hosts():
25+ filepath = os.path.join(_service_dir(), 'internal-hosts.json')
26+ internal_hosts = []
27+ if os.path.exists(filepath):
28+ with open(filepath, 'r') as f:
29+ internal_hosts = json.load(f)
30+
31+ return internal_hosts
32+
33+def _set_internal_hosts():
34+ config = charmhelpers.core.hookenv.config()
35+
36+ internal_hosts = _get_internal_hosts()
37+ internal_hosts += [x.strip() for x in
38+ config.get('internal_hosts').split(',') if x]
39+ internal_hosts = list(set(internal_hosts))
40+ if internal_hosts:
41+ juju_log("Adding internal whitelisted hosts: {}".format(
42+ internal_hosts))
43+ with open(os.path.join(_service_dir(),
44+ 'internal-hosts.json'), 'w') as f:
45+ json.dump(internal_hosts, f)
46+
47+
48 def _set_allowed_hosts():
49 config = charmhelpers.core.hookenv.config()
50 ips = [
51@@ -306,6 +330,7 @@
52 _create_client(relation['url'])
53
54 _set_allowed_hosts()
55+ _set_internal_hosts()
56 update_nrpe_config()
57 _set_logging()
58 _wsgi_reload()
59@@ -322,6 +347,7 @@
60 charmhelpers.core.hookenv.relation_set(
61 relation_id, {'port': config["port"], 'hostname': host})
62 _set_allowed_hosts()
63+ _set_internal_hosts()
64
65
66 @hooks.hook('json_status-relation-joined')
67@@ -523,7 +549,16 @@
68 @hooks.hook('oauth-server-relation-joined')
69 @hooks.hook('oauth-server-relation-changed')
70 def oauth_server_relation_joined_changed():
71+ config = charmhelpers.core.hookenv.config()
72+
73 relation = charmhelpers.core.hookenv.relation_get()
74+ internal_hosts = map(
75+ unicode.strip, config.get('internal_hosts', '').split(','))
76+ internal_hosts.append(relation.get('private-address'))
77+
78+ # use set() to make a unique list
79+ config['internal_hosts'] = ','.join(set(internal_hosts))
80+ config.save()
81
82 client_redirect_uri = relation.get('url')
83
84
85=== modified file 'charms/precise/wsgi-app/unit_tests/test_hooks.py'
86--- charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-10-16 13:24:26 +0000
87+++ charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-11-06 19:15:32 +0000
88@@ -254,6 +254,16 @@
89 self.assertTrue(
90 os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json')))
91
92+ def test__get_internal_hosts(self):
93+ self.assertEqual(0, len(hooks._get_internal_hosts()))
94+
95+ @mock.patch('hooks._get_internal_hosts')
96+ def test__set_internal_hosts(self, _get_internal_hosts):
97+ _get_internal_hosts.return_value = ['127.0.0.1']
98+ hooks._set_internal_hosts()
99+ self.assertTrue(
100+ os.path.exists(os.path.join(self.tmpdir, 'internal-hosts.json')))
101+
102
103 class TestWebsiteHooks(RestishTestCase):
104
105
106=== modified file 'ci-utils/ci_utils/tastypie/test.py'
107--- ci-utils/ci_utils/tastypie/test.py 2014-05-27 11:41:04 +0000
108+++ ci-utils/ci_utils/tastypie/test.py 2014-11-06 19:15:32 +0000
109@@ -16,22 +16,40 @@
110 import json
111 import mock
112
113+from django.contrib.contenttypes.models import ContentType
114+from django.contrib.auth.models import User, Permission
115 from tastypie.test import ResourceTestCase, TestApiClient
116
117
118 class TastypieTestCase(ResourceTestCase):
119 '''A base class for RESTFul services.'''
120
121- def setUp(self, resource_base):
122+ @mock.patch('ticket.models.Ticket.create_container')
123+ def setUp(self, resource_base, create_container):
124 '''Set up an admin user that can make POST/PATCH calls.'''
125+ super(TastypieTestCase, self).setUp()
126 self.resource_base = resource_base
127 if resource_base[-1] == '/':
128 self.resource_base = resource_base[:-1]
129 # create an api key for "post" operations
130 # we currently don't use authentication|authorization, so its nothing
131- self.auth = None
132 self.client = TestApiClient()
133
134+ self.username = "testuser"
135+ self.password = "testpass"
136+ create_container.return_value = None
137+ self.user = User.objects.create_user(username=self.username,
138+ password=self.password)
139+
140+ content_type = ContentType.objects.get(
141+ app_label='ticket', model='ticket',
142+ )
143+
144+ for codename in ['review_ticket', 'change_ticket', 'add_ticket']:
145+ permission = Permission.objects.get(
146+ codename=codename, content_type=content_type)
147+ self.user.user_permissions.add(permission)
148+
149 def _resource(self, resource):
150 if resource[0] == '/' or resource.startswith('http://'):
151 # assume the caller is passing the full path
152@@ -46,16 +64,18 @@
153 def post(self, resource, params):
154 '''Create the resource and return location of the new object.'''
155 resource = self._resource(resource)
156- resp = self.client.post(
157- resource, data=params, authentication=self.auth)
158+ self.client.client.login(username=self.username,
159+ password=self.password)
160+ resp = self.client.post(resource, data=params)
161 self.assertHttpCreated(resp)
162 return resp['location']
163
164 def patch(self, resource, params):
165 '''Update an existing resource.'''
166 resource = self._resource(resource)
167- resp = self.client.patch(
168- resource, data=params, authentication=self.auth)
169+ self.client.client.login(username=self.username,
170+ password=self.password)
171+ resp = self.client.patch(resource, data=params)
172 self.assertHttpAccepted(resp)
173
174 def getResource(self, resource, params={}):
175@@ -74,7 +94,9 @@
176 def delete(self, resource):
177 '''Delete an existing resource.'''
178 resource = self._resource(resource)
179- resp = self.client.delete(resource, authentication=self.auth)
180+ self.client.client.login(username=self.username,
181+ password=self.password)
182+ resp = self.client.delete(resource)
183 return resp
184
185
186
187=== modified file 'ticket_system/ticket/api.py'
188--- ticket_system/ticket/api.py 2014-11-06 05:57:57 +0000
189+++ ticket_system/ticket/api.py 2014-11-06 19:15:32 +0000
190@@ -60,6 +60,8 @@
191 )
192 from project.api import SourcePackageResource
193
194+from authorization import UserOrInternalAuthorization
195+
196
197 class PageNumberPaginator(Paginator):
198 """Copes with YUI 'page'-number based pagination."""
199@@ -223,7 +225,7 @@
200 class Meta:
201 queryset = SourcePackageUpload.objects.all()
202 allowed_methods = ['get', 'post']
203- authorization = Authorization()
204+ authorization = UserOrInternalAuthorization()
205 resource_name = 'spu'
206
207
208@@ -270,7 +272,7 @@
209 # authorization mechanisms, not on the local resource queryset.
210 queryset = Ticket.objects.filter(private=False)
211 allowed_methods = ['get', 'post', 'patch']
212- authorization = Authorization()
213+ authorization = UserOrInternalAuthorization()
214 filtering = {
215 "status": ALL,
216 }
217@@ -286,7 +288,7 @@
218 """
219
220 class Meta:
221- authorization = Authorization()
222+ authorization = UserOrInternalAuthorization()
223 list_allowed_methods = []
224 detail_allowed_methods = []
225 excludes = ['id']
226@@ -328,7 +330,7 @@
227 class Meta:
228 queryset = SubTicket.objects.all()
229 allowed_methods = ['get', 'post']
230- authorization = Authorization()
231+ authorization = UserOrInternalAuthorization()
232
233 def hydrate_sourcepackage(self, bundle):
234 """Re-use existing `SourcePackage` when creating new SubTickets."""
235@@ -360,7 +362,7 @@
236 class Meta:
237 queryset = TicketArtifact.objects.all()
238 allowed_methods = ['get', 'post']
239- authorization = Authorization()
240+ authorization = UserOrInternalAuthorization()
241 filtering = {
242 'type': ['exact'],
243 'ticket': ['exact'],
244@@ -375,7 +377,7 @@
245 class Meta:
246 queryset = SubTicketArtifact.objects.all()
247 allowed_methods = ['get', 'post']
248- authorization = Authorization()
249+ authorization = UserOrInternalAuthorization()
250
251
252 class FullTicketArtifactResource(ModelResource):
253@@ -447,7 +449,7 @@
254 queryset = SubTicket.objects.all()
255 fields = ['id', 'current_workflow_step', 'status']
256 allowed_methods = ['get', 'patch']
257- authorization = Authorization()
258+ authorization = UserOrInternalAuthorization()
259 resource_name = 'updatesubticketstatus'
260 filtering = {
261 "id": ('exact'),
262@@ -620,7 +622,7 @@
263
264 class Meta:
265 queryset = Review.objects.all()
266- authorization = Authorization()
267+ authorization = UserOrInternalAuthorization()
268 allowed_methods = ['get', 'post', 'patch', 'delete']
269 filtering = {
270 'ticket': ALL_WITH_RELATIONS,
271
272=== added file 'ticket_system/ticket/authorization.py'
273--- ticket_system/ticket/authorization.py 1970-01-01 00:00:00 +0000
274+++ ticket_system/ticket/authorization.py 2014-11-06 19:15:32 +0000
275@@ -0,0 +1,97 @@
276+# Ubuntu Continuous Integration Engine
277+# Copyright 2014 Canonical Ltd.
278+
279+# This program is free software: you can redistribute it and/or modify it
280+# under the terms of the GNU Affero General Public License version 3, as
281+# published by the Free Software Foundation.
282+
283+# This program is distributed in the hope that it will be useful, but
284+# WITHOUT ANY WARRANTY; without even the implied warranties of
285+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
286+# PURPOSE. See the GNU Affero General Public License for more details.
287+
288+# You should have received a copy of the GNU Affero General Public License
289+# along with this program. If not, see <http://www.gnu.org/licenses/>.
290+
291+from tastypie.authorization import Authorization
292+from tastypie.exceptions import Unauthorized
293+
294+from ticket_system import settings
295+from ticket.models import Review
296+
297+
298+class UserOrInternalAuthorization(Authorization):
299+ ''' Custom authorization class to control permissions for ticket
300+ creation, editing, and reviewing. Also allows a list of internal
301+ IPs.
302+
303+ Expected permissions:
304+ - ticket.add_ticket
305+ - ticket.change_ticket
306+ - ticket.review_ticket
307+ '''
308+
309+ def _all_access(self, request, perm):
310+ user = request.user
311+ return user.has_perm(perm) or self._internal_address(request)
312+
313+ def _internal_address(self, request):
314+ reload(settings)
315+ return request.META.get('REMOTE_ADDR') in settings.internal_hosts
316+
317+ def read_list(self, object_list, bundle):
318+ return object_list
319+
320+ def read_detail(self, object_list, bundle):
321+ return True
322+
323+ def create_list(self, object_list, bundle):
324+ if self._all_access(bundle.request, 'ticket.add_ticket'):
325+ return object_list
326+ else:
327+ raise Unauthorized("You don't have write access")
328+
329+ def create_detail(self, object_list, bundle):
330+ return self._all_access(bundle.request, 'ticket.add_ticket')
331+
332+ def update_list(self, object_list, bundle):
333+ # TODO: for now we inspect the object type and enforce review
334+ # permissions once we switch to the all-in-one ticket api we'll
335+ # need to rework this logic. <joe.talbott@canonical.com>
336+ if (object_list and object_list.count() > 0 and
337+ isinstance(object_list[0], Review)):
338+ if self._all_access(bundle.request, 'ticket.review_ticket'):
339+ return object_list
340+ else:
341+ raise Unauthorized("You don't have write access")
342+ else:
343+ if self._all_access(bundle.request, 'ticket.change_ticket'):
344+ return object_list
345+ else:
346+ raise Unauthorized("You don't have write access")
347+
348+ def update_detail(self, object_list, bundle):
349+ # TODO: for now we inspect the object type and enforce review
350+ # permissions once we switch to the all-in-one ticket api we'll
351+ # need to rework this logic. <joe.talbott@canonical.com>
352+ if (object_list and object_list.count() > 0 and
353+ isinstance(object_list[0], Review)):
354+ return self._all_access(bundle.request, 'ticket.review_ticket')
355+
356+ return self._all_access(bundle.request, 'ticket.change_ticket')
357+
358+ def delete_list(self, object_list, bundle):
359+ if (object_list and object_list.count() > 0 and
360+ isinstance(object_list[0], Review)):
361+ if self._all_access(bundle.request, 'ticket.review_ticket'):
362+ return object_list
363+
364+ raise Unauthorized("Sorry, no deletes.")
365+
366+ def delete_detail(self, object_list, bundle):
367+ if (object_list and object_list.count() > 0 and
368+ isinstance(object_list[0], Review)):
369+ if self._all_access(bundle.request, 'ticket.review_ticket'):
370+ return True
371+
372+ raise Unauthorized("Sorry, no deletes.")
373
374=== modified file 'ticket_system/ticket/models.py'
375--- ticket_system/ticket/models.py 2014-11-06 04:56:47 +0000
376+++ ticket_system/ticket/models.py 2014-11-06 19:15:32 +0000
377@@ -114,6 +114,9 @@
378 class Meta:
379 db_table = 'ticket'
380 ordering = ['id']
381+ permissions = (
382+ ('review_ticket', 'Can review tickets'),
383+ )
384
385 SERIES_CHOICES = tuple(
386 (series, series.upper()) for series in SUPPORTED_SERIES)
387
388=== modified file 'ticket_system/ticket/tests/__init__.py'
389--- ticket_system/ticket/tests/__init__.py 2014-10-14 15:10:23 +0000
390+++ ticket_system/ticket/tests/__init__.py 2014-11-06 19:15:32 +0000
391@@ -13,6 +13,7 @@
392 # You should have received a copy of the GNU Affero General Public License
393 # along with this program. If not, see <http://www.gnu.org/licenses/>.
394
395+from test_authorization import *
396 from test_commands import *
397 from test_full_read_api import *
398 from test_models import *
399
400=== added file 'ticket_system/ticket/tests/test_authorization.py'
401--- ticket_system/ticket/tests/test_authorization.py 1970-01-01 00:00:00 +0000
402+++ ticket_system/ticket/tests/test_authorization.py 2014-11-06 19:15:32 +0000
403@@ -0,0 +1,138 @@
404+# Ubuntu Continuous Integration Engine
405+# Copyright 2014 Canonical Ltd.
406+
407+# This program is free software: you can redistribute it and/or modify it
408+# under the terms of the GNU Affero General Public License version 3, as
409+# published by the Free Software Foundation.
410+
411+# This program is distributed in the hope that it will be useful, but
412+# WITHOUT ANY WARRANTY; without even the implied warranties of
413+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
414+# PURPOSE. See the GNU Affero General Public License for more details.
415+
416+# You should have received a copy of the GNU Affero General Public License
417+# along with this program. If not, see <http://www.gnu.org/licenses/>.
418+
419+import json
420+import os
421+import tempfile
422+
423+from django.contrib.contenttypes.models import ContentType
424+from django.contrib.auth.models import Permission
425+from ci_utils.tastypie.test import TicketTastypieTestCase
426+
427+from ticket.models import Ticket
428+from ticket_system import settings
429+
430+
431+class TestUserOrInternalAuthorization(TicketTastypieTestCase):
432+
433+ def _setup_internal_ips(self):
434+ self.tmpfile = tempfile.NamedTemporaryFile()
435+ json.dump(['127.0.0.1'], self.tmpfile)
436+ self.tmpfile.flush()
437+
438+ def get_credentials(self):
439+ return ""
440+
441+ def _get_content_type(self):
442+ return ContentType.objects.get(
443+ app_label='ticket', model='ticket',
444+ )
445+
446+ def _get_review_permission(self):
447+ return Permission.objects.get(codename='review_ticket',
448+ content_type=self._get_content_type())
449+
450+ def setUp(self):
451+ super(TestUserOrInternalAuthorization, self).setUp('/api/v1/')
452+
453+ self.ticket = Ticket.objects.create(title="blah", description="blah")
454+ self.assert_(self.ticket)
455+
456+ self.detail_url = "/api/v1/ticket/{0}/".format(self.ticket.uuid)
457+ self.post_data = {
458+ 'id': '{}'.format(self.ticket.pk),
459+ }
460+
461+ def test_ticket_detail_read_authenticated(self):
462+ self.api_client.client.login(username=self.username,
463+ password=self.password)
464+ response = self.client.get(self.detail_url, format='json',
465+ authentication=self.get_credentials())
466+ self.assertEqual(200, response.status_code)
467+
468+ def test_ticket_detail_read_internal_ip(self):
469+ self._setup_internal_ips()
470+ os.environ['TS_INTERNAL_IPS_PATH'] = self.tmpfile.name
471+ reload(settings)
472+ response = self.client.get(self.detail_url, format='json')
473+ if self.tmpfile:
474+ self.tmpfile.close()
475+ self.assertEqual(200, response.status_code)
476+
477+ def test_ticket_detail_read_unauthenticated(self):
478+ """ Test that unauthenticated read requests are allowed. """
479+ response = self.client.get(self.detail_url, format='json')
480+ self.assertEqual(200, response.status_code)
481+
482+ def test_ticket_read_authenticated(self):
483+ self.api_client.client.login(username=self.username,
484+ password=self.password)
485+ response = self.client.get("/api/v1/ticket/",
486+ authentication=self.get_credentials())
487+ self.assertEqual(200, response.status_code)
488+
489+ def test_ticket_read_unauthenticated(self):
490+ response = self.client.get("/api/v1/ticket/")
491+ self.assertEqual(200, response.status_code)
492+
493+ def skipped_test_ticket_write_unauthenticated(self):
494+ self.assertHttpUnauthorized(self.api_client.post('/api/v1/ticket/',
495+ format='json',
496+ data=self.post_data))
497+
498+ def test_ticket_write_authenticated(self):
499+ data = {}
500+
501+ self.api_client.client.login(
502+ username=self.username, password=self.password)
503+ response = self.api_client.post('/api/v1/ticket/', format='json',
504+ data=data,
505+ authentication=self.get_credentials())
506+ self.assertEqual(201, response.status_code)
507+
508+ # Make sure a ticket was added
509+ response = self.api_client.get("/api/v1/ticket/",
510+ authentication=self.get_credentials())
511+ self.assertEqual(200, response.status_code)
512+
513+ self.assertEqual(2, len(json.loads(response.content)['objects']))
514+
515+ def test_ticket_review_authenticated(self):
516+ data = {'review_type': 'publishing', 'status': 'Doing stuff'}
517+
518+ self.ticket.review_set.create(workflow_step=0)
519+
520+ self.api_client.client.login(
521+ username=self.username, password=self.password)
522+ url = "/api/v1/review/{}/".format(self.ticket.review_set.all()[0].id)
523+ response = self.api_client.patch(url, format='json',
524+ data=data,
525+ authentication=self.get_credentials())
526+ self.assertEqual(202, response.status_code)
527+
528+ def test_ticket_review_no_perm_authenticated(self):
529+ data = {'review_type': 'publishing', 'status': 'Doing stuff'}
530+
531+ self.user.user_permissions.remove(self._get_review_permission())
532+
533+ self.ticket.review_set.create(workflow_step=0)
534+
535+ self.api_client.client.login(
536+ username=self.username, password=self.password)
537+ url = "/api/v1/review/{}/".format(self.ticket.review_set.all()[0].id)
538+ response = self.api_client.patch(url, format='json',
539+ data=data,
540+ authentication=self.get_credentials())
541+ self.assertEqual(401, response.status_code)
542
543=== modified file 'ticket_system/ticket/tests/test_write_api.py'
544--- ticket_system/ticket/tests/test_write_api.py 2014-11-06 00:05:55 +0000
545+++ ticket_system/ticket/tests/test_write_api.py 2014-11-06 19:15:32 +0000
546@@ -329,8 +329,7 @@
547 def test_patch_detail(self):
548 # `Ticket` object can be patched via the API.
549 new_data = {'owner': 'ci@example.com'}
550- resp = self.client.patch('/api/v1/' + self.detail_url, data=new_data)
551- self.assertHttpAccepted(resp)
552+ self.patch('/api/v1/' + self.detail_url, new_data)
553 self.assertEqual(
554 new_data['owner'], Ticket.objects.get(pk=self.ticket.pk).owner)
555 self.assertEqual(Ticket.objects.count(), 1)
556
557=== modified file 'ticket_system/ticket_system/settings.py'
558--- ticket_system/ticket_system/settings.py 2014-10-07 10:04:01 +0000
559+++ ticket_system/ticket_system/settings.py 2014-11-06 19:15:32 +0000
560@@ -249,6 +249,13 @@
561 with open(allowed) as f:
562 ALLOWED_HOSTS = json.load(f)
563
564+internal_hosts = []
565+internal = os.environ.get('TS_INTERNAL_IPS_PATH')
566+if internal and os.path.exists(internal):
567+ with open(internal) as f:
568+ json_str = f.read()
569+ internal_hosts = json.loads(json_str)
570+
571 logging = os.path.join(BASEDIR, '../../../logging.json')
572 if os.path.exists(logging):
573 data = {}

Subscribers

People subscribed via source and target branches