Merge lp:~mvo/ubuntu-webcatalog/machine-inventory into lp:ubuntu-webcatalog
- machine-inventory
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Michael Foord |
Approved revision: | 32 |
Merged at revision: | 29 |
Proposed branch: | lp:~mvo/ubuntu-webcatalog/machine-inventory |
Merge into: | lp:ubuntu-webcatalog |
Diff against target: |
772 lines (+677/-1) 11 files modified
requirements.txt (+5/-0) src/webcatalog/api/__init__.py (+15/-0) src/webcatalog/api/handlers.py (+34/-0) src/webcatalog/api/urls.py (+37/-0) src/webcatalog/auth.py (+158/-0) src/webcatalog/models/__init__.py (+36/-0) src/webcatalog/models/applications.py (+10/-0) src/webcatalog/models/oauthtoken.py (+212/-0) src/webcatalog/tests/test_handlers.py (+37/-0) src/webcatalog/urls.py (+4/-1) src/webcatalog/utilities.py (+129/-0) |
To merge this branch: | bzr merge lp:~mvo/ubuntu-webcatalog/machine-inventory |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Foord (community) | Approve | ||
Review via email: mp+66094@code.launchpad.net |
Commit message
Adds a stub API and integration into ubuntu-sso for the webcatalog (refactoring the layout of models).
Description of the change
Adds a stub API and integration into ubuntu-sso for the webcatalog
- 30. By Michael Vogt
-
merged from trunk and resolve conflicts
Michael Vogt (mvo) wrote : | # |
Michael Foord (mfoord) wrote : | # |
I dislike the use of the following in the template:
from __future__ import absolute_import
__metaclass__ = type
If __metaclass__ = type ever *does* anything then the code it affects should be changed, and if it doesn't do anything then it shouldn't be used. absolute_import should only be used if it is needed (in my opinion). For example: class WebServices (line 724 of diff) would be better as a new style class explicitly rather than relying on __metaclass__ = type.
To be honest I also dislike the boilerplate approach to starting the api - it means you have a lot of unused imports (for example) in handlers.py. I would use pyflakes and only import what you need - adding new things as you actually need them. (I have a pathological hatred of adding code - even temporarily - for imaginary future use cases.) Same with test_handlers.py.
Not my app though, so feel free to ignore that.
In WebService, failing to create a ServiceRoot is not a fatal error. What is the rationale for this? Is it so that an api creation failure does not bring down the whole app?
- 31. By Michael Vogt
-
fix pyflakes warnings in the new code
Michael Nelson (michael.nelson) wrote : | # |
On Wed, Jun 29, 2011 at 1:29 PM, Michael Foord
<email address hidden>wrote:
> I dislike the use of the following in the template:
>
> from __future__ import absolute_import
> __metaclass__ = type
>
> If __metaclass__ = type ever *does* anything then the code it affects
> should be changed, and if it doesn't do anything then it shouldn't be used.
> absolute_import should only be used if it is needed (in my opinion). For
> example: class WebServices (line 724 of diff) would be better as a new style
> class explicitly rather than relying on __metaclass__ = type.
>
>
Heh... as per the review you did for me Michael (hah! 3 michael's having a
conversation), these are just part of the new_file_
think any of us care, we just need to update the template and all the
current files in one go (ie. a separate branch). I don't think it makes
sense to be changing one file here and another there depending on who's
reviewing.
> To be honest I also dislike the boilerplate approach to starting the api -
> it means you have a lot of unused imports (for example) in handlers.py. I
> would use pyflakes and only import what you need - adding new things as you
> actually need them. (I have a pathological hatred of adding code - even
> temporarily - for imaginary future use cases.) Same with test_handlers.py.
>
>
+1 for removing unused imports, but as far as landing this branch - that was
my suggestion to Michael, as he (necessarily) re-organised the models
module, which means that the sooner he lands that, the less headache he'll
have resolving conflicts later (as we can all work with the reorganised
code).
> Not my app though, so feel free to ignore that.
>
> In WebService, failing to create a ServiceRoot is not a fatal error. What
> is the rationale for this? Is it so that an api creation failure does not
> bring down the whole app?
>
> --
>
> https:/
> You are subscribed to branch lp:ubuntu-webcatalog.
>
--
Michael Nelson
Canonical Ltd.
+49 176 491 53481 (mob)
<email address hidden>
IRC nick: noodles (noodles775 on Freenode)
Ubuntu - Linux for human beings | www.ubuntu.com | www.canonical.com
- 32. By Michael Vogt
-
src/webcatalog/
utilities. py: make pyflakes clean
Michael Vogt (mvo) wrote : | # |
Thanks a bunch Michael for this review! I fixed the unused imports now and will talk about the metaclass issue and the serviceroot one with the rnrserver maintainers.
Preview Diff
1 | === modified file 'requirements.txt' |
2 | --- requirements.txt 2011-06-18 23:17:11 +0000 |
3 | +++ requirements.txt 2011-06-29 11:42:41 +0000 |
4 | @@ -2,6 +2,8 @@ |
5 | coverage |
6 | django |
7 | django-configglue==0.4 |
8 | +django-openid-auth==0.2 |
9 | +django-piston |
10 | -e bzr+http://bazaar.launchpad.net/~canonical-isd-hackers/django-pgtools/trunk |
11 | django-preflight |
12 | mock |
13 | @@ -10,3 +12,6 @@ |
14 | python-openid |
15 | setuptools |
16 | south |
17 | +oauth |
18 | +httplib2 |
19 | +lazr.restfulclient |
20 | |
21 | === added directory 'src/webcatalog/api' |
22 | === added file 'src/webcatalog/api/__init__.py' |
23 | --- src/webcatalog/api/__init__.py 1970-01-01 00:00:00 +0000 |
24 | +++ src/webcatalog/api/__init__.py 2011-06-29 11:42:41 +0000 |
25 | @@ -0,0 +1,15 @@ |
26 | +# This file is part of the Ubuntu Web Catalog |
27 | +# Copyright (C) 2011 Canonical Ltd. |
28 | +# |
29 | +# This program is free software: you can redistribute it and/or modify |
30 | +# it under the terms of the GNU Affero General Public License as |
31 | +# published by the Free Software Foundation, either version 3 of the |
32 | +# License, or (at your option) any later version. |
33 | +# |
34 | +# This program is distributed in the hope that it will be useful, |
35 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
36 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
37 | +# GNU Affero General Public License for more details. |
38 | +# |
39 | +# You should have received a copy of the GNU Affero General Public License |
40 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
41 | |
42 | === added file 'src/webcatalog/api/handlers.py' |
43 | --- src/webcatalog/api/handlers.py 1970-01-01 00:00:00 +0000 |
44 | +++ src/webcatalog/api/handlers.py 2011-06-29 11:42:41 +0000 |
45 | @@ -0,0 +1,34 @@ |
46 | +# -*- coding: utf-8 -*- |
47 | +# This file is part of the Ubuntu Web Catalog |
48 | +# Copyright (C) 2011 Canonical Ltd. |
49 | +# |
50 | +# This program is free software: you can redistribute it and/or modify |
51 | +# it under the terms of the GNU Affero General Public License as |
52 | +# published by the Free Software Foundation, either version 3 of the |
53 | +# License, or (at your option) any later version. |
54 | +# |
55 | +# This program is distributed in the hope that it will be useful, |
56 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
57 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
58 | +# GNU Affero General Public License for more details. |
59 | +# |
60 | +# You should have received a copy of the GNU Affero General Public License |
61 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
62 | + |
63 | +"""Piston handlers for the Ubuntu Web Catalog API.""" |
64 | + |
65 | +from __future__ import absolute_import |
66 | + |
67 | +__metaclass__ = type |
68 | +__all__ = [ |
69 | + 'ServerStatusHandler', |
70 | +] |
71 | + |
72 | +from piston.handler import BaseHandler |
73 | + |
74 | +class ServerStatusHandler(BaseHandler): |
75 | + allowed_methods = ('GET',) |
76 | + |
77 | + def read(self, request): |
78 | + return "ok" |
79 | + |
80 | |
81 | === added file 'src/webcatalog/api/urls.py' |
82 | --- src/webcatalog/api/urls.py 1970-01-01 00:00:00 +0000 |
83 | +++ src/webcatalog/api/urls.py 2011-06-29 11:42:41 +0000 |
84 | @@ -0,0 +1,37 @@ |
85 | +# This file is part of the Ubuntu Web Catalog |
86 | +# Copyright (C) 2011 Canonical Ltd. |
87 | +# |
88 | +# This program is free software: you can redistribute it and/or modify |
89 | +# it under the terms of the GNU Affero General Public License as |
90 | +# published by the Free Software Foundation, either version 3 of the |
91 | +# License, or (at your option) any later version. |
92 | +# |
93 | +# This program is distributed in the hope that it will be useful, |
94 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
95 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
96 | +# GNU Affero General Public License for more details. |
97 | +# |
98 | +# You should have received a copy of the GNU Affero General Public License |
99 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
100 | + |
101 | +from django.conf.urls.defaults import url, patterns |
102 | + |
103 | +from piston.resource import Resource |
104 | +from webcatalog.api.handlers import ( |
105 | + ServerStatusHandler, |
106 | +) |
107 | +from webcatalog.auth import SSOOAuthAuthentication |
108 | + |
109 | +auth = SSOOAuthAuthentication(realm="Ubuntu Web Catalog") |
110 | + |
111 | +server_status_resource = Resource(handler=ServerStatusHandler) |
112 | + |
113 | +urlpatterns = patterns('', |
114 | + # get status of the service (usually just "ok", might be "read-only") |
115 | + # to ensure we inform the user when he wants to write a review or |
116 | + # send a moderation request |
117 | + # GET /1.0/server-status/ |
118 | + url(r'^1.0/server-status/$', server_status_resource, |
119 | + name='server-status'), |
120 | + |
121 | +) |
122 | |
123 | === added file 'src/webcatalog/auth.py' |
124 | --- src/webcatalog/auth.py 1970-01-01 00:00:00 +0000 |
125 | +++ src/webcatalog/auth.py 2011-06-29 11:42:41 +0000 |
126 | @@ -0,0 +1,158 @@ |
127 | +# This file is part of the Ubuntu Web Catalog |
128 | +# Copyright (C) 2011 Canonical Ltd. |
129 | +# |
130 | +# This program is free software: you can redistribute it and/or modify |
131 | +# it under the terms of the GNU Affero General Public License as |
132 | +# published by the Free Software Foundation, either version 3 of the |
133 | +# License, or (at your option) any later version. |
134 | +# |
135 | +# This program is distributed in the hope that it will be useful, |
136 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
137 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
138 | +# GNU Affero General Public License for more details. |
139 | +# |
140 | +# You should have received a copy of the GNU Affero General Public License |
141 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
142 | + |
143 | +"""SSO/OAuth authenticator for Piston.""" |
144 | + |
145 | + |
146 | +from __future__ import absolute_import |
147 | + |
148 | +__metaclass__ = type |
149 | +__all__ = [ |
150 | + 'SSOOAuthAuthentication', |
151 | + ] |
152 | + |
153 | +from datetime import datetime, timedelta |
154 | +from django.conf import settings |
155 | +from django.contrib.auth.models import User |
156 | +from django.http import HttpResponse |
157 | +from oauth import oauth |
158 | +from piston.authentication import OAuthAuthentication, oauth_datastore |
159 | +from piston.oauth import OAuthError |
160 | + |
161 | +from webcatalog.models import Consumer, Token |
162 | +from webcatalog.utilities import ( |
163 | + WebServices, |
164 | + full_claimed_id, |
165 | +) |
166 | + |
167 | +TOKEN_CACHE_EXPIRY = timedelta(hours= |
168 | + getattr(settings, 'TOKEN_CACHE_EXPIRY_HOURS', 4)) |
169 | + |
170 | +class SSOOAuthAuthentication(OAuthAuthentication): |
171 | + """ This class is a Piston Authentication class. |
172 | + |
173 | + See http://bitbucket.org/jespern/django-piston/wiki/Documentation for |
174 | + more info. |
175 | + """ |
176 | + def __init__(self, realm='API'): |
177 | + self.realm = realm |
178 | + |
179 | + def challenge(self): |
180 | + """ We define our own challenge so that we can provide our own realm |
181 | + and return a simple error message instead of rendering a template. |
182 | + """ |
183 | + resp = HttpResponse("Authorization Required") |
184 | + resp['WWW-Authenticate'] = 'OAuth realm="%s"' % self.realm |
185 | + resp.status_code = 401 |
186 | + return resp |
187 | + |
188 | + def is_authenticated(self, request): |
189 | + """ We override is_authenticated so we can prefetch creds from SSO |
190 | + before the oauth mechanism asks us for them |
191 | + """ |
192 | + headers = { |
193 | + 'Authorization' : request.META.get('HTTP_AUTHORIZATION', '') |
194 | + } |
195 | + orequest = oauth.OAuthRequest.from_request( |
196 | + request.method, request.build_absolute_uri(), headers=headers, |
197 | + query_string=request.META['QUERY_STRING']) |
198 | + if (orequest is None or |
199 | + not 'oauth_token' in orequest.parameters or |
200 | + not 'oauth_consumer_key' in orequest.parameters): |
201 | + return False |
202 | + |
203 | + self.prefetch_oauth_consumer(orequest) |
204 | + if self.is_valid_request(request): |
205 | + try: |
206 | + consumer, token, parameters = self.validate_token(request) |
207 | + except OAuthError, err: |
208 | + return False |
209 | + if consumer and token: |
210 | + request.user = token.user |
211 | + request.throttle_extra = token.consumer.id |
212 | + return True |
213 | + return False |
214 | + |
215 | + def prefetch_oauth_consumer(self, request): |
216 | + """Check if we have a consumer for the current creds, and validate |
217 | + the token with SSO if we don't. |
218 | + """ |
219 | + web_services = WebServices() |
220 | + oauthtoken = request.get_parameter('oauth_token') |
221 | + consumer_key = request.get_parameter('oauth_consumer_key') |
222 | + tokens = Token.objects.filter(token=oauthtoken, |
223 | + consumer__key=consumer_key) |
224 | + if len(tokens) == 0 or (tokens[0].updated_at < |
225 | + datetime.now() - TOKEN_CACHE_EXPIRY): |
226 | + pieces = web_services.get_data_for_account(token=oauthtoken, |
227 | + openid_identifier=consumer_key, signature=request.get_parameter('oauth_signature')) |
228 | + if not pieces: |
229 | + return |
230 | + Consumer.objects.filter(key=consumer_key).exclude( |
231 | + secret=pieces['consumer_secret']).delete() |
232 | + claimed_id = full_claimed_id(consumer_key) |
233 | + try: |
234 | + user = User.objects.get( |
235 | + useropenid__claimed_id=claimed_id) |
236 | + except User.DoesNotExist: |
237 | + if (not pieces.get('preferred_email') or |
238 | + not pieces.get('username')): |
239 | + return |
240 | + user = User.objects.create_user(pieces['username'], |
241 | + pieces['preferred_email'], password=None) |
242 | + user.useropenid_set.create(claimed_id=claimed_id) |
243 | + |
244 | + displayname = pieces['displayname'] |
245 | + if displayname != user.get_full_name(): |
246 | + user.first_name, sep, user.last_name = displayname.rpartition( |
247 | + ' ') |
248 | + user.save() |
249 | + |
250 | + consumer, created = Consumer.objects.get_or_create(user=user, |
251 | + key=consumer_key, secret=pieces['consumer_secret']) |
252 | + token, created = Token.objects.get_or_create(consumer=consumer, |
253 | + token=pieces['token'], token_secret=pieces['token_secret'], |
254 | + name=pieces['name']) |
255 | + if not created: |
256 | + # Update updated_at |
257 | + token.save() |
258 | + |
259 | + @staticmethod |
260 | + def validate_token(request, check_timestamp=True, check_nonce=True): |
261 | + oauth_server, oauth_request = initialize_server_request(request) |
262 | + if oauth_server is None: |
263 | + raise OAuthError('initialize_server_request returned None') |
264 | + return oauth_server.verify_request(oauth_request) |
265 | + |
266 | +def initialize_server_request(request): |
267 | + """ |
268 | + Shortcut for initialization. |
269 | + """ |
270 | + headers = { |
271 | + 'Authorization' : request.META.get('HTTP_AUTHORIZATION', '') |
272 | + } |
273 | + oauth_request = oauth.OAuthRequest.from_request( |
274 | + request.method, request.build_absolute_uri(), headers=headers, |
275 | + query_string=request.META['QUERY_STRING']) |
276 | + |
277 | + if oauth_request: |
278 | + oauth_server = oauth.OAuthServer(oauth_datastore(oauth_request)) |
279 | + oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT()) |
280 | + oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1()) |
281 | + else: |
282 | + oauth_server = None |
283 | + |
284 | + return oauth_server, oauth_request |
285 | |
286 | === added directory 'src/webcatalog/models' |
287 | === added file 'src/webcatalog/models/__init__.py' |
288 | --- src/webcatalog/models/__init__.py 1970-01-01 00:00:00 +0000 |
289 | +++ src/webcatalog/models/__init__.py 2011-06-29 11:42:41 +0000 |
290 | @@ -0,0 +1,36 @@ |
291 | +# This file is part of the Ubuntu Web Catalog |
292 | +# Copyright (C) 2011 Canonical Ltd. |
293 | +# |
294 | +# This program is free software: you can redistribute it and/or modify |
295 | +# it under the terms of the GNU Affero General Public License as |
296 | +# published by the Free Software Foundation, either version 3 of the |
297 | +# License, or (at your option) any later version. |
298 | +# |
299 | +# This program is distributed in the hope that it will be useful, |
300 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
301 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
302 | +# GNU Affero General Public License for more details. |
303 | +# |
304 | +# You should have received a copy of the GNU Affero General Public License |
305 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
306 | + |
307 | +from __future__ import absolute_import |
308 | + |
309 | +__all__ = [ |
310 | + # oauth |
311 | + 'Token', |
312 | + 'Consumer', |
313 | + 'Nonce', |
314 | + 'DataStore', |
315 | + # applications |
316 | + 'DistroSeries', |
317 | + 'Application', |
318 | + 'Department', |
319 | +] |
320 | + |
321 | +from .oauthtoken import Token, Consumer, Nonce, DataStore |
322 | +from .applications import ( |
323 | + DistroSeries, |
324 | + Application, |
325 | + Department, |
326 | +) |
327 | |
328 | === renamed file 'src/webcatalog/models.py' => 'src/webcatalog/models/applications.py' |
329 | --- src/webcatalog/models.py 2011-06-28 15:03:12 +0000 |
330 | +++ src/webcatalog/models/applications.py 2011-06-29 11:42:41 +0000 |
331 | @@ -45,6 +45,9 @@ |
332 | def __unicode__(self): |
333 | return "Ubuntu %s (%s)" % (self.code_name.capitalize(), self.version) |
334 | |
335 | + class Meta: |
336 | + app_label = 'webcatalog' |
337 | + |
338 | |
339 | class Application(models.Model): |
340 | # We'll most likely need one of these per lang/series (as each lang |
341 | @@ -130,6 +133,10 @@ |
342 | args=[self.distroseries.code_name, self.package_name])}) |
343 | return crumbs |
344 | |
345 | + class Meta: |
346 | + app_label = 'webcatalog' |
347 | + |
348 | + |
349 | class Department(models.Model): |
350 | parent = models.ForeignKey('self', blank=True, null=True) |
351 | name = models.CharField(max_length=64) |
352 | @@ -149,3 +156,6 @@ |
353 | crumbs.append({'name': self.name, 'url': reverse('wc-department', |
354 | args=[self.id])}) |
355 | return crumbs |
356 | + |
357 | + class Meta: |
358 | + app_label = 'webcatalog' |
359 | |
360 | === added file 'src/webcatalog/models/oauthtoken.py' |
361 | --- src/webcatalog/models/oauthtoken.py 1970-01-01 00:00:00 +0000 |
362 | +++ src/webcatalog/models/oauthtoken.py 2011-06-29 11:42:41 +0000 |
363 | @@ -0,0 +1,212 @@ |
364 | +# This file is part of the Ubuntu Web Catalog |
365 | +# Copyright (C) 2011 Canonical Ltd. |
366 | +# |
367 | +# This program is free software: you can redistribute it and/or modify |
368 | +# it under the terms of the GNU Affero General Public License as |
369 | +# published by the Free Software Foundation, either version 3 of the |
370 | +# License, or (at your option) any later version. |
371 | +# |
372 | +# This program is distributed in the hope that it will be useful, |
373 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
374 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
375 | +# GNU Affero General Public License for more details. |
376 | +# |
377 | +# You should have received a copy of the GNU Affero General Public License |
378 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
379 | + |
380 | +"""Models related to OAuth authentication""" |
381 | + |
382 | +from __future__ import absolute_import |
383 | + |
384 | +__metaclass__ = type |
385 | +__all__ = [ |
386 | + 'Token', |
387 | + 'Consumer', |
388 | + 'Nonce', |
389 | + 'DataStore', |
390 | + ] |
391 | + |
392 | +import os.path |
393 | +import random |
394 | +import struct |
395 | +import string |
396 | +from functools import partial |
397 | +from logging import getLogger |
398 | + |
399 | +from oauth.oauth import OAuthToken, OAuthDataStore, OAuthConsumer |
400 | + |
401 | +from django.contrib.auth.models import User |
402 | +from django.db import models |
403 | + |
404 | + |
405 | +TOKEN_LENGTH = 50 |
406 | +TOKEN_SECRET_LENGTH = 50 |
407 | +CONSUMER_SECRET_LENGTH = 30 |
408 | + |
409 | + |
410 | +def _set_seed(): |
411 | + if (not hasattr(_set_seed, 'seed') and |
412 | + os.path.exists("/dev/random")): |
413 | + |
414 | + data = open("/dev/random").read(struct.calcsize('Q')) |
415 | + random.seed(struct.unpack('Q', data)) |
416 | + _set_seed.seed = True |
417 | + |
418 | + |
419 | +def generate_random_string(length): |
420 | + _set_seed() |
421 | + return ''.join(random.choice(string.ascii_letters) |
422 | + for x in range(length)) |
423 | + |
424 | + |
425 | +class Token(models.Model): |
426 | + consumer = models.ForeignKey('Consumer') |
427 | + |
428 | + token = models.CharField( |
429 | + max_length=TOKEN_LENGTH, |
430 | + default=partial(generate_random_string, TOKEN_LENGTH), |
431 | + primary_key=True) |
432 | + |
433 | + token_secret = models.CharField( |
434 | + max_length=TOKEN_SECRET_LENGTH, |
435 | + default=partial(generate_random_string, TOKEN_SECRET_LENGTH)) |
436 | + |
437 | + name = models.CharField(max_length=255, blank=True) |
438 | + |
439 | + created_at = models.DateTimeField(auto_now_add=True) |
440 | + updated_at = models.DateTimeField(auto_now=True) |
441 | + |
442 | + def oauth_token(self): |
443 | + """Return OAuthToken with information contained in this model""" |
444 | + return OAuthToken(self.token, self.token_secret) |
445 | + |
446 | + def __unicode__(self): |
447 | + return self.token |
448 | + |
449 | + class Meta: |
450 | + app_label = 'reviewsapp' |
451 | + |
452 | + |
453 | +class Consumer(models.Model): |
454 | + user = models.OneToOneField(User, related_name='oauth_consumer') |
455 | + |
456 | + key = models.CharField(max_length=64) |
457 | + |
458 | + secret = models.CharField(max_length=255, blank=True, null=False, |
459 | + default=partial(generate_random_string, CONSUMER_SECRET_LENGTH)) |
460 | + |
461 | + created_at = models.DateTimeField(auto_now_add=True) |
462 | + updated_at = models.DateTimeField(auto_now=True) |
463 | + |
464 | + @classmethod |
465 | + def lookup_consumer(self, consumer_key): |
466 | + # lukasz: To my best knowledge the query below can return more than one |
467 | + # entry only if the OpenID provider was switched for already deployed |
468 | + # code (between staging/production SSO). That would mean, that the same |
469 | + # user on SSO will get second Django user, and second Consumer object, |
470 | + # but with exactly the same consumer_key. |
471 | + consumers = Consumer.objects.filter(key=consumer_key) |
472 | + # If more than one Consumer object is found we choose the one created |
473 | + # as the last one keeping up with the assumption of switching the |
474 | + # OpenID providers. |
475 | + consumers = consumers.order_by('-created_at') |
476 | + consumers_count = consumers.count() |
477 | + |
478 | + if consumers_count == 0: |
479 | + return None |
480 | + elif consumers_count > 1: |
481 | + getLogger(__name__).info( |
482 | + "consumer_key=%s has %d entries in the database", |
483 | + consumer_key, consumers_count |
484 | + ) |
485 | + consumer = consumers[0] |
486 | + return consumer |
487 | + |
488 | + def __unicode__(self): |
489 | + return self.key |
490 | + |
491 | + def oauth_consumer(self): |
492 | + """Return OAuthConsumer based on information contained in this model""" |
493 | + return OAuthConsumer(self.key, self.secret) |
494 | + |
495 | + class Meta: |
496 | + app_label = 'reviewsapp' |
497 | + |
498 | + |
499 | +class Nonce(models.Model): |
500 | + token = models.ForeignKey(Token) |
501 | + consumer = models.ForeignKey(Consumer) |
502 | + nonce = models.CharField(max_length=255, unique=True, editable=False) |
503 | + created_at = models.DateTimeField(auto_now_add=True) |
504 | + |
505 | + @classmethod |
506 | + def create(cls, consumer_key, token_value, nonce): |
507 | + """ |
508 | + Create new nonce object linked to given consumer and token |
509 | + """ |
510 | + consumer = Consumer.lookup_consumer(consumer_key) |
511 | + token = consumer.token_set.get(token=token_value) |
512 | + return consumer.nonce_set.create(token=token, nonce=nonce) |
513 | + |
514 | + class Meta: |
515 | + app_label = 'reviewsapp' |
516 | + |
517 | +class DataStore(OAuthDataStore): |
518 | + |
519 | + def lookup_token(self, token_type, token_field): |
520 | + """ |
521 | + :param token_type: type of token to lookup |
522 | + :param token_field: token to look up |
523 | + |
524 | + :note: token_type should always be 'access' as only such tokens are |
525 | + stored in database |
526 | + |
527 | + :returns: OAuthToken object |
528 | + """ |
529 | + assert token_type == 'access' |
530 | + |
531 | + try: |
532 | + token = Token.objects.get(token=token_field) |
533 | + # Piston expects OAuth tokens to have 'consumer' and 'user' atts. |
534 | + # (see piston.authentication.OAuthAuthentication.is_authenticated) |
535 | + oauthtoken = OAuthToken(token.token, token.token_secret) |
536 | + oauthtoken.consumer = token.consumer |
537 | + oauthtoken.user = token.consumer.user |
538 | + return oauthtoken |
539 | + except Token.DoesNotExist: |
540 | + return None |
541 | + |
542 | + def lookup_consumer(self, consumer_key): |
543 | + """ |
544 | + :param consumer_key: consumer key to lookup |
545 | + |
546 | + :returns: OAuthConsumer object |
547 | + """ |
548 | + consumer = Consumer.lookup_consumer(consumer_key) |
549 | + if consumer is not None: |
550 | + return consumer.oauth_consumer() |
551 | + |
552 | + |
553 | + def lookup_nonce(self, consumer, token, nonce): |
554 | + """ |
555 | + :param consumer: OAuthConsumer object |
556 | + :param token: OAuthToken object |
557 | + :param nonce: nonce to check |
558 | + |
559 | + """ |
560 | + count = Nonce.objects.filter( |
561 | + consumer__key=consumer.key, |
562 | + token__token=token.key, |
563 | + nonce=nonce).count() |
564 | + if count > 0: |
565 | + return True |
566 | + else: |
567 | + Nonce.create(consumer.key, token.key, nonce) |
568 | + return False |
569 | + |
570 | + def __init__(self, oauth_request=None): |
571 | + """To serve as a Piston datastore we'll need provide this signature. |
572 | + |
573 | + We later won't use the oauth_request, so we can ignore it here. |
574 | + """ |
575 | + return super(DataStore, self).__init__() |
576 | |
577 | === added file 'src/webcatalog/tests/test_handlers.py' |
578 | --- src/webcatalog/tests/test_handlers.py 1970-01-01 00:00:00 +0000 |
579 | +++ src/webcatalog/tests/test_handlers.py 2011-06-29 11:42:41 +0000 |
580 | @@ -0,0 +1,37 @@ |
581 | +# -*- coding: utf-8 -*- |
582 | +# This file is part of the Ubuntu Web Catalog |
583 | +# Copyright (C) 2011 Canonical Ltd. |
584 | +# |
585 | +# This program is free software: you can redistribute it and/or modify |
586 | +# it under the terms of the GNU Affero General Public License as |
587 | +# published by the Free Software Foundation, either version 3 of the |
588 | +# License, or (at your option) any later version. |
589 | +# |
590 | +# This program is distributed in the hope that it will be useful, |
591 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
592 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
593 | +# GNU Affero General Public License for more details. |
594 | +# |
595 | +# You should have received a copy of the GNU Affero General Public License |
596 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
597 | + |
598 | +"""Ubuntu Web Catalog handlers tests.""" |
599 | + |
600 | +from __future__ import absolute_import |
601 | + |
602 | +__metaclass__ = type |
603 | +__all__ = [ |
604 | + 'ServerStatusHandlerTestCase', |
605 | + ] |
606 | + |
607 | + |
608 | +from django.test import TestCase |
609 | + |
610 | +from reviewsapp.api.handlers import ( |
611 | + ServerStatusHandler, |
612 | +) |
613 | + |
614 | +class ServerStatusHandlerTestCase(TestCase): |
615 | + def test_read(self): |
616 | + ss_handler = ServerStatusHandler() |
617 | + self.assertEqual('ok', ss_handler.read(None)) |
618 | |
619 | === modified file 'src/webcatalog/urls.py' |
620 | --- src/webcatalog/urls.py 2011-06-29 08:06:05 +0000 |
621 | +++ src/webcatalog/urls.py 2011-06-29 11:42:41 +0000 |
622 | @@ -21,7 +21,7 @@ |
623 | absolute_import, |
624 | with_statement, |
625 | ) |
626 | -from django.conf.urls.defaults import patterns, url |
627 | +from django.conf.urls.defaults import patterns, include, url |
628 | |
629 | __metaclass__ = type |
630 | __all__ = [ |
631 | @@ -38,4 +38,7 @@ |
632 | url(r'^applications/(?P<package_name>[-.+\w]+)/$', 'application_detail', |
633 | name="wc-package-detail"), |
634 | url(r'^search/$', 'search', name="wc-search"), |
635 | + |
636 | + (r'^api/', include('webcatalog.api.urls')), |
637 | + |
638 | ) |
639 | |
640 | === added file 'src/webcatalog/utilities.py' |
641 | --- src/webcatalog/utilities.py 1970-01-01 00:00:00 +0000 |
642 | +++ src/webcatalog/utilities.py 2011-06-29 11:42:41 +0000 |
643 | @@ -0,0 +1,129 @@ |
644 | +# This file is part of the Ubuntu Web Catalog |
645 | +# Copyright (C) 2011 Canonical Ltd. |
646 | +# |
647 | +# This program is free software: you can redistribute it and/or modify |
648 | +# it under the terms of the GNU Affero General Public License as |
649 | +# published by the Free Software Foundation, either version 3 of the |
650 | +# License, or (at your option) any later version. |
651 | +# |
652 | +# This program is distributed in the hope that it will be useful, |
653 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
654 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
655 | +# GNU Affero General Public License for more details. |
656 | +# |
657 | +# You should have received a copy of the GNU Affero General Public License |
658 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
659 | + |
660 | +"""SSO/OAuth authenticator for Piston.""" |
661 | + |
662 | + |
663 | +from __future__ import absolute_import |
664 | + |
665 | +__metaclass__ = type |
666 | +__all__ = [ |
667 | + 'WebServices', |
668 | + 'WebServiceError', |
669 | + ] |
670 | + |
671 | +import logging |
672 | +import urllib |
673 | +from httplib2 import ServerNotFoundError |
674 | + |
675 | +from django.conf import settings |
676 | + |
677 | +from lazr.restfulclient.authorize import BasicHttpAuthorizer |
678 | +from lazr.restfulclient.authorize.oauth import OAuthAuthorizer |
679 | +from lazr.restfulclient.errors import HTTPError |
680 | +from lazr.restfulclient.resource import ServiceRoot |
681 | +from oauth.oauth import OAuthToken |
682 | + |
683 | +class WebServiceError(Exception): |
684 | + """Raised when we cannot connect to a web service.""" |
685 | + |
686 | + |
687 | +class WebServices: |
688 | + _identity_provider = None |
689 | + |
690 | + def __init__(self): |
691 | + """Pre-create the identity provider webservice if it will be used.""" |
692 | + if settings.PRELOAD_API_SERVICE_ROOTS: |
693 | + self._set_identity_provider() |
694 | + |
695 | + @property |
696 | + def logger(self): |
697 | + return logging.getLogger('rnr.utilities') |
698 | + |
699 | + def _create_service(self, authorizer, service_root_url): |
700 | + try: |
701 | + service_root = ServiceRoot(authorizer, service_root_url) |
702 | + self.logger.info( |
703 | + "Webservice root creation successful: %s" % service_root_url) |
704 | + return service_root |
705 | + except (HTTPError, ServerNotFoundError), e: |
706 | + self.logger.exception( |
707 | + "Failed to create service root: %s" % service_root_url) |
708 | + return None |
709 | + |
710 | + def _create_basic_auth_service(self, service_root_url, username, password): |
711 | + authorizer = BasicHttpAuthorizer(username, password) |
712 | + return self._create_service(authorizer, service_root_url) |
713 | + |
714 | + def _set_identity_provider(self): |
715 | + WebServices._identity_provider = self._create_basic_auth_service( |
716 | + settings.SSO_API_SERVICE_ROOT, settings.SSO_API_AUTH_USERNAME, |
717 | + settings.SSO_API_AUTH_PASSWORD) |
718 | + |
719 | + @property |
720 | + def identity_provider(self): |
721 | + if self._identity_provider is None: |
722 | + self._set_identity_provider() |
723 | + |
724 | + if self._identity_provider is None: |
725 | + raise WebServiceError("The payment service is not available.") |
726 | + |
727 | + return self._identity_provider |
728 | + |
729 | + def _fake_validate_token(self, token, openid_identifier, signature): |
730 | + """ This is a version of validate_token that gets the |
731 | + token_secret, consumer_secret from the plaintext signature. |
732 | + |
733 | + It will break once we use a real signature for oauth, but |
734 | + its very useful for testing as it does not require the special |
735 | + priviledges required for the call to |
736 | + identity_provider.authentications.validate_token() |
737 | + |
738 | + The token can still be validated (and will be) with a |
739 | + api.accounts.me() call |
740 | + """ |
741 | + (consumer_secret, token_secret) = urllib.unquote(signature).split("&") |
742 | + result = {} |
743 | + result["token"] = token |
744 | + result["token_secret"] = token_secret |
745 | + result["consumer_key"] = openid_identifier |
746 | + result["consumer_secret"] = consumer_secret |
747 | + result["name"] = "fake-token" |
748 | + return result |
749 | + |
750 | + def get_data_for_account(self, token, openid_identifier, signature=None): |
751 | + if settings.SSO_AUTH_MODE_NO_UBUNTU_SSO_PLAINTEXT_ONLY: |
752 | + # token extraction based on PLAINTEXT token, the function |
753 | + # "validate_token" is really a "get_secrets_for_token()" call |
754 | + result = self._fake_validate_token( |
755 | + token, openid_identifier, signature) |
756 | + else: |
757 | + result = self.identity_provider.authentications.validate_token( |
758 | + token=token, consumer_key=openid_identifier) |
759 | + |
760 | + if result: |
761 | + oauth_token = OAuthToken(result['token'], result['token_secret']) |
762 | + authorizer = OAuthAuthorizer(result['consumer_key'], |
763 | + result['consumer_secret'], oauth_token) |
764 | + api = self._create_service(authorizer, |
765 | + settings.SSO_API_SERVICE_ROOT) |
766 | + result.update(api.accounts.me()) |
767 | + return result |
768 | + |
769 | + |
770 | +def full_claimed_id(consumer_key): |
771 | + return '%s/+id/%s' % (settings.OPENID_SSO_SERVER_URL.strip('/'), |
772 | + consumer_key) |
This adds the boilerplatte needed in order to add a API to the webcatalog. We want to expand this later to add API to store/retrieve the installed package list of the user. This shall allow better recommendations in the future and other useful features.
Please note that there is some duplicated code here that I cargo-culted from rnrserver. Like the utilities class. I'm not sure what the best way is to get rid of this duplication.