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
=== modified file 'charms/precise/wsgi-app/config.yaml'
--- charms/precise/wsgi-app/config.yaml 2014-10-15 10:51:10 +0000
+++ charms/precise/wsgi-app/config.yaml 2014-11-06 19:15:32 +0000
@@ -63,6 +63,11 @@
63 default: "INFO"63 default: "INFO"
64 description: The logging level.64 description: The logging level.
6565
66 internal_hosts:
67 type: string
68 default: ''
69 description: A comma separated list of internal hosts that don't need permissions checked. Ticket-system only.
70
66 # Apt info used by charmhelpers71 # Apt info used by charmhelpers
67 install_sources:72 install_sources:
68 type: string73 type: string
6974
=== modified file 'charms/precise/wsgi-app/hooks/hooks.py'
--- charms/precise/wsgi-app/hooks/hooks.py 2014-10-16 13:24:26 +0000
+++ charms/precise/wsgi-app/hooks/hooks.py 2014-11-06 19:15:32 +0000
@@ -244,6 +244,30 @@
244 return reverseproxies244 return reverseproxies
245245
246246
247def _get_internal_hosts():
248 filepath = os.path.join(_service_dir(), 'internal-hosts.json')
249 internal_hosts = []
250 if os.path.exists(filepath):
251 with open(filepath, 'r') as f:
252 internal_hosts = json.load(f)
253
254 return internal_hosts
255
256def _set_internal_hosts():
257 config = charmhelpers.core.hookenv.config()
258
259 internal_hosts = _get_internal_hosts()
260 internal_hosts += [x.strip() for x in
261 config.get('internal_hosts').split(',') if x]
262 internal_hosts = list(set(internal_hosts))
263 if internal_hosts:
264 juju_log("Adding internal whitelisted hosts: {}".format(
265 internal_hosts))
266 with open(os.path.join(_service_dir(),
267 'internal-hosts.json'), 'w') as f:
268 json.dump(internal_hosts, f)
269
270
247def _set_allowed_hosts():271def _set_allowed_hosts():
248 config = charmhelpers.core.hookenv.config()272 config = charmhelpers.core.hookenv.config()
249 ips = [273 ips = [
@@ -306,6 +330,7 @@
306 _create_client(relation['url'])330 _create_client(relation['url'])
307331
308 _set_allowed_hosts()332 _set_allowed_hosts()
333 _set_internal_hosts()
309 update_nrpe_config()334 update_nrpe_config()
310 _set_logging()335 _set_logging()
311 _wsgi_reload()336 _wsgi_reload()
@@ -322,6 +347,7 @@
322 charmhelpers.core.hookenv.relation_set(347 charmhelpers.core.hookenv.relation_set(
323 relation_id, {'port': config["port"], 'hostname': host})348 relation_id, {'port': config["port"], 'hostname': host})
324 _set_allowed_hosts()349 _set_allowed_hosts()
350 _set_internal_hosts()
325351
326352
327@hooks.hook('json_status-relation-joined')353@hooks.hook('json_status-relation-joined')
@@ -523,7 +549,16 @@
523@hooks.hook('oauth-server-relation-joined')549@hooks.hook('oauth-server-relation-joined')
524@hooks.hook('oauth-server-relation-changed')550@hooks.hook('oauth-server-relation-changed')
525def oauth_server_relation_joined_changed():551def oauth_server_relation_joined_changed():
552 config = charmhelpers.core.hookenv.config()
553
526 relation = charmhelpers.core.hookenv.relation_get()554 relation = charmhelpers.core.hookenv.relation_get()
555 internal_hosts = map(
556 unicode.strip, config.get('internal_hosts', '').split(','))
557 internal_hosts.append(relation.get('private-address'))
558
559 # use set() to make a unique list
560 config['internal_hosts'] = ','.join(set(internal_hosts))
561 config.save()
527562
528 client_redirect_uri = relation.get('url')563 client_redirect_uri = relation.get('url')
529564
530565
=== modified file 'charms/precise/wsgi-app/unit_tests/test_hooks.py'
--- charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-10-16 13:24:26 +0000
+++ charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-11-06 19:15:32 +0000
@@ -254,6 +254,16 @@
254 self.assertTrue(254 self.assertTrue(
255 os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json')))255 os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json')))
256256
257 def test__get_internal_hosts(self):
258 self.assertEqual(0, len(hooks._get_internal_hosts()))
259
260 @mock.patch('hooks._get_internal_hosts')
261 def test__set_internal_hosts(self, _get_internal_hosts):
262 _get_internal_hosts.return_value = ['127.0.0.1']
263 hooks._set_internal_hosts()
264 self.assertTrue(
265 os.path.exists(os.path.join(self.tmpdir, 'internal-hosts.json')))
266
257267
258class TestWebsiteHooks(RestishTestCase):268class TestWebsiteHooks(RestishTestCase):
259269
260270
=== modified file 'ci-utils/ci_utils/tastypie/test.py'
--- ci-utils/ci_utils/tastypie/test.py 2014-05-27 11:41:04 +0000
+++ ci-utils/ci_utils/tastypie/test.py 2014-11-06 19:15:32 +0000
@@ -16,22 +16,40 @@
16import json16import json
17import mock17import mock
1818
19from django.contrib.contenttypes.models import ContentType
20from django.contrib.auth.models import User, Permission
19from tastypie.test import ResourceTestCase, TestApiClient21from tastypie.test import ResourceTestCase, TestApiClient
2022
2123
22class TastypieTestCase(ResourceTestCase):24class TastypieTestCase(ResourceTestCase):
23 '''A base class for RESTFul services.'''25 '''A base class for RESTFul services.'''
2426
25 def setUp(self, resource_base):27 @mock.patch('ticket.models.Ticket.create_container')
28 def setUp(self, resource_base, create_container):
26 '''Set up an admin user that can make POST/PATCH calls.'''29 '''Set up an admin user that can make POST/PATCH calls.'''
30 super(TastypieTestCase, self).setUp()
27 self.resource_base = resource_base31 self.resource_base = resource_base
28 if resource_base[-1] == '/':32 if resource_base[-1] == '/':
29 self.resource_base = resource_base[:-1]33 self.resource_base = resource_base[:-1]
30 # create an api key for "post" operations34 # create an api key for "post" operations
31 # we currently don't use authentication|authorization, so its nothing35 # we currently don't use authentication|authorization, so its nothing
32 self.auth = None
33 self.client = TestApiClient()36 self.client = TestApiClient()
3437
38 self.username = "testuser"
39 self.password = "testpass"
40 create_container.return_value = None
41 self.user = User.objects.create_user(username=self.username,
42 password=self.password)
43
44 content_type = ContentType.objects.get(
45 app_label='ticket', model='ticket',
46 )
47
48 for codename in ['review_ticket', 'change_ticket', 'add_ticket']:
49 permission = Permission.objects.get(
50 codename=codename, content_type=content_type)
51 self.user.user_permissions.add(permission)
52
35 def _resource(self, resource):53 def _resource(self, resource):
36 if resource[0] == '/' or resource.startswith('http://'):54 if resource[0] == '/' or resource.startswith('http://'):
37 # assume the caller is passing the full path55 # assume the caller is passing the full path
@@ -46,16 +64,18 @@
46 def post(self, resource, params):64 def post(self, resource, params):
47 '''Create the resource and return location of the new object.'''65 '''Create the resource and return location of the new object.'''
48 resource = self._resource(resource)66 resource = self._resource(resource)
49 resp = self.client.post(67 self.client.client.login(username=self.username,
50 resource, data=params, authentication=self.auth)68 password=self.password)
69 resp = self.client.post(resource, data=params)
51 self.assertHttpCreated(resp)70 self.assertHttpCreated(resp)
52 return resp['location']71 return resp['location']
5372
54 def patch(self, resource, params):73 def patch(self, resource, params):
55 '''Update an existing resource.'''74 '''Update an existing resource.'''
56 resource = self._resource(resource)75 resource = self._resource(resource)
57 resp = self.client.patch(76 self.client.client.login(username=self.username,
58 resource, data=params, authentication=self.auth)77 password=self.password)
78 resp = self.client.patch(resource, data=params)
59 self.assertHttpAccepted(resp)79 self.assertHttpAccepted(resp)
6080
61 def getResource(self, resource, params={}):81 def getResource(self, resource, params={}):
@@ -74,7 +94,9 @@
74 def delete(self, resource):94 def delete(self, resource):
75 '''Delete an existing resource.'''95 '''Delete an existing resource.'''
76 resource = self._resource(resource)96 resource = self._resource(resource)
77 resp = self.client.delete(resource, authentication=self.auth)97 self.client.client.login(username=self.username,
98 password=self.password)
99 resp = self.client.delete(resource)
78 return resp100 return resp
79101
80102
81103
=== modified file 'ticket_system/ticket/api.py'
--- ticket_system/ticket/api.py 2014-11-06 05:57:57 +0000
+++ ticket_system/ticket/api.py 2014-11-06 19:15:32 +0000
@@ -60,6 +60,8 @@
60)60)
61from project.api import SourcePackageResource61from project.api import SourcePackageResource
6262
63from authorization import UserOrInternalAuthorization
64
6365
64class PageNumberPaginator(Paginator):66class PageNumberPaginator(Paginator):
65 """Copes with YUI 'page'-number based pagination."""67 """Copes with YUI 'page'-number based pagination."""
@@ -223,7 +225,7 @@
223 class Meta:225 class Meta:
224 queryset = SourcePackageUpload.objects.all()226 queryset = SourcePackageUpload.objects.all()
225 allowed_methods = ['get', 'post']227 allowed_methods = ['get', 'post']
226 authorization = Authorization()228 authorization = UserOrInternalAuthorization()
227 resource_name = 'spu'229 resource_name = 'spu'
228230
229231
@@ -270,7 +272,7 @@
270 # authorization mechanisms, not on the local resource queryset.272 # authorization mechanisms, not on the local resource queryset.
271 queryset = Ticket.objects.filter(private=False)273 queryset = Ticket.objects.filter(private=False)
272 allowed_methods = ['get', 'post', 'patch']274 allowed_methods = ['get', 'post', 'patch']
273 authorization = Authorization()275 authorization = UserOrInternalAuthorization()
274 filtering = {276 filtering = {
275 "status": ALL,277 "status": ALL,
276 }278 }
@@ -286,7 +288,7 @@
286 """288 """
287289
288 class Meta:290 class Meta:
289 authorization = Authorization()291 authorization = UserOrInternalAuthorization()
290 list_allowed_methods = []292 list_allowed_methods = []
291 detail_allowed_methods = []293 detail_allowed_methods = []
292 excludes = ['id']294 excludes = ['id']
@@ -328,7 +330,7 @@
328 class Meta:330 class Meta:
329 queryset = SubTicket.objects.all()331 queryset = SubTicket.objects.all()
330 allowed_methods = ['get', 'post']332 allowed_methods = ['get', 'post']
331 authorization = Authorization()333 authorization = UserOrInternalAuthorization()
332334
333 def hydrate_sourcepackage(self, bundle):335 def hydrate_sourcepackage(self, bundle):
334 """Re-use existing `SourcePackage` when creating new SubTickets."""336 """Re-use existing `SourcePackage` when creating new SubTickets."""
@@ -360,7 +362,7 @@
360 class Meta:362 class Meta:
361 queryset = TicketArtifact.objects.all()363 queryset = TicketArtifact.objects.all()
362 allowed_methods = ['get', 'post']364 allowed_methods = ['get', 'post']
363 authorization = Authorization()365 authorization = UserOrInternalAuthorization()
364 filtering = {366 filtering = {
365 'type': ['exact'],367 'type': ['exact'],
366 'ticket': ['exact'],368 'ticket': ['exact'],
@@ -375,7 +377,7 @@
375 class Meta:377 class Meta:
376 queryset = SubTicketArtifact.objects.all()378 queryset = SubTicketArtifact.objects.all()
377 allowed_methods = ['get', 'post']379 allowed_methods = ['get', 'post']
378 authorization = Authorization()380 authorization = UserOrInternalAuthorization()
379381
380382
381class FullTicketArtifactResource(ModelResource):383class FullTicketArtifactResource(ModelResource):
@@ -447,7 +449,7 @@
447 queryset = SubTicket.objects.all()449 queryset = SubTicket.objects.all()
448 fields = ['id', 'current_workflow_step', 'status']450 fields = ['id', 'current_workflow_step', 'status']
449 allowed_methods = ['get', 'patch']451 allowed_methods = ['get', 'patch']
450 authorization = Authorization()452 authorization = UserOrInternalAuthorization()
451 resource_name = 'updatesubticketstatus'453 resource_name = 'updatesubticketstatus'
452 filtering = {454 filtering = {
453 "id": ('exact'),455 "id": ('exact'),
@@ -620,7 +622,7 @@
620622
621 class Meta:623 class Meta:
622 queryset = Review.objects.all()624 queryset = Review.objects.all()
623 authorization = Authorization()625 authorization = UserOrInternalAuthorization()
624 allowed_methods = ['get', 'post', 'patch', 'delete']626 allowed_methods = ['get', 'post', 'patch', 'delete']
625 filtering = {627 filtering = {
626 'ticket': ALL_WITH_RELATIONS,628 'ticket': ALL_WITH_RELATIONS,
627629
=== added file 'ticket_system/ticket/authorization.py'
--- ticket_system/ticket/authorization.py 1970-01-01 00:00:00 +0000
+++ ticket_system/ticket/authorization.py 2014-11-06 19:15:32 +0000
@@ -0,0 +1,97 @@
1# Ubuntu Continuous Integration Engine
2# Copyright 2014 Canonical Ltd.
3
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16from tastypie.authorization import Authorization
17from tastypie.exceptions import Unauthorized
18
19from ticket_system import settings
20from ticket.models import Review
21
22
23class UserOrInternalAuthorization(Authorization):
24 ''' Custom authorization class to control permissions for ticket
25 creation, editing, and reviewing. Also allows a list of internal
26 IPs.
27
28 Expected permissions:
29 - ticket.add_ticket
30 - ticket.change_ticket
31 - ticket.review_ticket
32 '''
33
34 def _all_access(self, request, perm):
35 user = request.user
36 return user.has_perm(perm) or self._internal_address(request)
37
38 def _internal_address(self, request):
39 reload(settings)
40 return request.META.get('REMOTE_ADDR') in settings.internal_hosts
41
42 def read_list(self, object_list, bundle):
43 return object_list
44
45 def read_detail(self, object_list, bundle):
46 return True
47
48 def create_list(self, object_list, bundle):
49 if self._all_access(bundle.request, 'ticket.add_ticket'):
50 return object_list
51 else:
52 raise Unauthorized("You don't have write access")
53
54 def create_detail(self, object_list, bundle):
55 return self._all_access(bundle.request, 'ticket.add_ticket')
56
57 def update_list(self, object_list, bundle):
58 # TODO: for now we inspect the object type and enforce review
59 # permissions once we switch to the all-in-one ticket api we'll
60 # need to rework this logic. <joe.talbott@canonical.com>
61 if (object_list and object_list.count() > 0 and
62 isinstance(object_list[0], Review)):
63 if self._all_access(bundle.request, 'ticket.review_ticket'):
64 return object_list
65 else:
66 raise Unauthorized("You don't have write access")
67 else:
68 if self._all_access(bundle.request, 'ticket.change_ticket'):
69 return object_list
70 else:
71 raise Unauthorized("You don't have write access")
72
73 def update_detail(self, object_list, bundle):
74 # TODO: for now we inspect the object type and enforce review
75 # permissions once we switch to the all-in-one ticket api we'll
76 # need to rework this logic. <joe.talbott@canonical.com>
77 if (object_list and object_list.count() > 0 and
78 isinstance(object_list[0], Review)):
79 return self._all_access(bundle.request, 'ticket.review_ticket')
80
81 return self._all_access(bundle.request, 'ticket.change_ticket')
82
83 def delete_list(self, object_list, bundle):
84 if (object_list and object_list.count() > 0 and
85 isinstance(object_list[0], Review)):
86 if self._all_access(bundle.request, 'ticket.review_ticket'):
87 return object_list
88
89 raise Unauthorized("Sorry, no deletes.")
90
91 def delete_detail(self, object_list, bundle):
92 if (object_list and object_list.count() > 0 and
93 isinstance(object_list[0], Review)):
94 if self._all_access(bundle.request, 'ticket.review_ticket'):
95 return True
96
97 raise Unauthorized("Sorry, no deletes.")
098
=== modified file 'ticket_system/ticket/models.py'
--- ticket_system/ticket/models.py 2014-11-06 04:56:47 +0000
+++ ticket_system/ticket/models.py 2014-11-06 19:15:32 +0000
@@ -114,6 +114,9 @@
114 class Meta:114 class Meta:
115 db_table = 'ticket'115 db_table = 'ticket'
116 ordering = ['id']116 ordering = ['id']
117 permissions = (
118 ('review_ticket', 'Can review tickets'),
119 )
117120
118 SERIES_CHOICES = tuple(121 SERIES_CHOICES = tuple(
119 (series, series.upper()) for series in SUPPORTED_SERIES)122 (series, series.upper()) for series in SUPPORTED_SERIES)
120123
=== modified file 'ticket_system/ticket/tests/__init__.py'
--- ticket_system/ticket/tests/__init__.py 2014-10-14 15:10:23 +0000
+++ ticket_system/ticket/tests/__init__.py 2014-11-06 19:15:32 +0000
@@ -13,6 +13,7 @@
13# You should have received a copy of the GNU Affero General Public License13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.14# along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16from test_authorization import *
16from test_commands import *17from test_commands import *
17from test_full_read_api import *18from test_full_read_api import *
18from test_models import *19from test_models import *
1920
=== added file 'ticket_system/ticket/tests/test_authorization.py'
--- ticket_system/ticket/tests/test_authorization.py 1970-01-01 00:00:00 +0000
+++ ticket_system/ticket/tests/test_authorization.py 2014-11-06 19:15:32 +0000
@@ -0,0 +1,138 @@
1# Ubuntu Continuous Integration Engine
2# Copyright 2014 Canonical Ltd.
3
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import json
17import os
18import tempfile
19
20from django.contrib.contenttypes.models import ContentType
21from django.contrib.auth.models import Permission
22from ci_utils.tastypie.test import TicketTastypieTestCase
23
24from ticket.models import Ticket
25from ticket_system import settings
26
27
28class TestUserOrInternalAuthorization(TicketTastypieTestCase):
29
30 def _setup_internal_ips(self):
31 self.tmpfile = tempfile.NamedTemporaryFile()
32 json.dump(['127.0.0.1'], self.tmpfile)
33 self.tmpfile.flush()
34
35 def get_credentials(self):
36 return ""
37
38 def _get_content_type(self):
39 return ContentType.objects.get(
40 app_label='ticket', model='ticket',
41 )
42
43 def _get_review_permission(self):
44 return Permission.objects.get(codename='review_ticket',
45 content_type=self._get_content_type())
46
47 def setUp(self):
48 super(TestUserOrInternalAuthorization, self).setUp('/api/v1/')
49
50 self.ticket = Ticket.objects.create(title="blah", description="blah")
51 self.assert_(self.ticket)
52
53 self.detail_url = "/api/v1/ticket/{0}/".format(self.ticket.uuid)
54 self.post_data = {
55 'id': '{}'.format(self.ticket.pk),
56 }
57
58 def test_ticket_detail_read_authenticated(self):
59 self.api_client.client.login(username=self.username,
60 password=self.password)
61 response = self.client.get(self.detail_url, format='json',
62 authentication=self.get_credentials())
63 self.assertEqual(200, response.status_code)
64
65 def test_ticket_detail_read_internal_ip(self):
66 self._setup_internal_ips()
67 os.environ['TS_INTERNAL_IPS_PATH'] = self.tmpfile.name
68 reload(settings)
69 response = self.client.get(self.detail_url, format='json')
70 if self.tmpfile:
71 self.tmpfile.close()
72 self.assertEqual(200, response.status_code)
73
74 def test_ticket_detail_read_unauthenticated(self):
75 """ Test that unauthenticated read requests are allowed. """
76 response = self.client.get(self.detail_url, format='json')
77 self.assertEqual(200, response.status_code)
78
79 def test_ticket_read_authenticated(self):
80 self.api_client.client.login(username=self.username,
81 password=self.password)
82 response = self.client.get("/api/v1/ticket/",
83 authentication=self.get_credentials())
84 self.assertEqual(200, response.status_code)
85
86 def test_ticket_read_unauthenticated(self):
87 response = self.client.get("/api/v1/ticket/")
88 self.assertEqual(200, response.status_code)
89
90 def skipped_test_ticket_write_unauthenticated(self):
91 self.assertHttpUnauthorized(self.api_client.post('/api/v1/ticket/',
92 format='json',
93 data=self.post_data))
94
95 def test_ticket_write_authenticated(self):
96 data = {}
97
98 self.api_client.client.login(
99 username=self.username, password=self.password)
100 response = self.api_client.post('/api/v1/ticket/', format='json',
101 data=data,
102 authentication=self.get_credentials())
103 self.assertEqual(201, response.status_code)
104
105 # Make sure a ticket was added
106 response = self.api_client.get("/api/v1/ticket/",
107 authentication=self.get_credentials())
108 self.assertEqual(200, response.status_code)
109
110 self.assertEqual(2, len(json.loads(response.content)['objects']))
111
112 def test_ticket_review_authenticated(self):
113 data = {'review_type': 'publishing', 'status': 'Doing stuff'}
114
115 self.ticket.review_set.create(workflow_step=0)
116
117 self.api_client.client.login(
118 username=self.username, password=self.password)
119 url = "/api/v1/review/{}/".format(self.ticket.review_set.all()[0].id)
120 response = self.api_client.patch(url, format='json',
121 data=data,
122 authentication=self.get_credentials())
123 self.assertEqual(202, response.status_code)
124
125 def test_ticket_review_no_perm_authenticated(self):
126 data = {'review_type': 'publishing', 'status': 'Doing stuff'}
127
128 self.user.user_permissions.remove(self._get_review_permission())
129
130 self.ticket.review_set.create(workflow_step=0)
131
132 self.api_client.client.login(
133 username=self.username, password=self.password)
134 url = "/api/v1/review/{}/".format(self.ticket.review_set.all()[0].id)
135 response = self.api_client.patch(url, format='json',
136 data=data,
137 authentication=self.get_credentials())
138 self.assertEqual(401, response.status_code)
0139
=== modified file 'ticket_system/ticket/tests/test_write_api.py'
--- ticket_system/ticket/tests/test_write_api.py 2014-11-06 00:05:55 +0000
+++ ticket_system/ticket/tests/test_write_api.py 2014-11-06 19:15:32 +0000
@@ -329,8 +329,7 @@
329 def test_patch_detail(self):329 def test_patch_detail(self):
330 # `Ticket` object can be patched via the API.330 # `Ticket` object can be patched via the API.
331 new_data = {'owner': 'ci@example.com'}331 new_data = {'owner': 'ci@example.com'}
332 resp = self.client.patch('/api/v1/' + self.detail_url, data=new_data)332 self.patch('/api/v1/' + self.detail_url, new_data)
333 self.assertHttpAccepted(resp)
334 self.assertEqual(333 self.assertEqual(
335 new_data['owner'], Ticket.objects.get(pk=self.ticket.pk).owner)334 new_data['owner'], Ticket.objects.get(pk=self.ticket.pk).owner)
336 self.assertEqual(Ticket.objects.count(), 1)335 self.assertEqual(Ticket.objects.count(), 1)
337336
=== modified file 'ticket_system/ticket_system/settings.py'
--- ticket_system/ticket_system/settings.py 2014-10-07 10:04:01 +0000
+++ ticket_system/ticket_system/settings.py 2014-11-06 19:15:32 +0000
@@ -249,6 +249,13 @@
249 with open(allowed) as f:249 with open(allowed) as f:
250 ALLOWED_HOSTS = json.load(f)250 ALLOWED_HOSTS = json.load(f)
251251
252internal_hosts = []
253internal = os.environ.get('TS_INTERNAL_IPS_PATH')
254if internal and os.path.exists(internal):
255 with open(internal) as f:
256 json_str = f.read()
257 internal_hosts = json.loads(json_str)
258
252logging = os.path.join(BASEDIR, '../../../logging.json')259logging = os.path.join(BASEDIR, '../../../logging.json')
253if os.path.exists(logging):260if os.path.exists(logging):
254 data = {}261 data = {}

Subscribers

People subscribed via source and target branches