Merge lp:~joetalbott/uci-engine/user_auth into lp:uci-engine
- user_auth
- Merge into trunk
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 |
Related bugs: |
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.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:833
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Celso Providelo (cprov) wrote : | # |
I have some questions inline that needs clarification, I am probably misunderstanding things.
- 834. By Joe Talbott
-
ticket-system - Add authentication tests
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:834
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 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.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:837
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 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
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:840
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 841. By Joe Talbott
-
ci-utils - Pep8 fixes for tastypie tests
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:841
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Celso Providelo (cprov) wrote : | # |
Joe,
It looks pretty good, I just have few inline comments to be addresses/discussed about testing infrastructure.
- 842. By Joe Talbott
-
ticket-system - Clean up tests and remove oauth bits.
- 843. By Joe Talbott
-
merge with trunk
* resolve conflicts
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:843
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 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()
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:847
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Celso Providelo (cprov) wrote : | # |
Thanks for the changes, Joe.
We are very close! Please address my inline comments and the test failure.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:847
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 848. By Joe Talbott
-
Use TicketTastypieT
estCase and ticket.uuid rather than .pk.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:848
http://
Executed test runs:
Click here to trigger a rebuild:
http://
- 849. By Joe Talbott
-
ticket-system - flake8 cleanup.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:849
http://
Executed test runs:
Click here to trigger a rebuild:
http://
Unmerged revisions
- 849. By Joe Talbott
-
ticket-system - flake8 cleanup.
- 848. By Joe Talbott
-
Use TicketTastypieT
estCase 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
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 = {} |
minor comment, otherwise looks like a good start to me