Merge lp:~mvo/ubuntu-webcatalog/machine-inventory into lp:ubuntu-webcatalog

Proposed by Michael Vogt
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
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

To post a comment you must log in.
30. By Michael Vogt

merged from trunk and resolve conflicts

Revision history for this message
Michael Vogt (mvo) wrote :

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.

Revision history for this message
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

Revision history for this message
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_template.py... I don't
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://code.launchpad.net/~mvo/ubuntu-webcatalog/machine-inventory/+merge/66094<https://code.launchpad.net/%7Emvo/ubuntu-webcatalog/machine-inventory/+merge/66094>
> 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

Revision history for this message
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.

Revision history for this message
Michael Foord (mfoord) wrote :

Approved :-)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches