Merge lp:~canonical-isd-hackers/canonical-identity-provider/compatible-piston into lp:canonical-identity-provider/release

Proposed by Anthony Lenton
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: no longer in the source branch.
Merged at revision: 151
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/compatible-piston
Merge into: lp:canonical-identity-provider/release
Diff against target: 3535 lines (+2090/-1018)
30 files modified
debian/control (+1/-10)
django_project/config.example/settings.py (+1/-7)
doctests/stories/api-authentications.txt (+4/-4)
doctests/stories/api-workflows.txt (+5/-2)
identityprovider/api10/decorators.py (+36/-0)
identityprovider/api10/forms.py (+55/-0)
identityprovider/api10/handlers.py (+410/-0)
identityprovider/api10/urls.py (+41/-0)
identityprovider/auth.py (+26/-0)
identityprovider/models/account.py (+0/-14)
identityprovider/preflight.py (+2/-2)
identityprovider/store.py (+35/-0)
identityprovider/templates/wadl1.0.xml (+1378/-0)
identityprovider/tests/test_forms.py (+4/-10)
identityprovider/tests/test_handlers.py (+79/-45)
identityprovider/tests/test_models_account.py (+0/-11)
identityprovider/tests/test_models_api.py (+0/-21)
identityprovider/tests/test_wsgi.py (+0/-71)
identityprovider/urls.py (+5/-0)
identityprovider/webservice/__init__.py (+0/-2)
identityprovider/webservice/forms.py (+0/-65)
identityprovider/webservice/interfaces.py (+0/-191)
identityprovider/webservice/models.py (+0/-389)
identityprovider/webservice/site.zcml (+0/-15)
identityprovider/wsgi.py (+1/-84)
mockservice/sso_mockserver/mockserver.py (+5/-5)
payload/__init__.py (+0/-8)
requirements.txt (+0/-53)
scripts/create_env (+1/-1)
setup.py (+1/-8)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/compatible-piston
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski (community) Needs Information
Canonical ISD hackers Pending
Review via email: mp+50668@code.launchpad.net

Commit message

Implement a lazr.restfulclient-compatible piston api.

Description of the change

Overview
========
This branch implements a backwards-compatible Piston api, replacing our current lazr.restful one.

Details
=======
The lazr.restful dependency is removed, and zope.interface is now only needed for running the tests, as zope.testbrowser is still used.

SSO's test suite was changed as little as possible for this branch, to ensure backwards-compatibility. A few tests were changed though, as they were quite lazr-specific or imported the webservice module that was removed completely.

This branch has been tested interactively using lazr.restfulclient. Ideally we should also test our main higher-level clients (like the desktop ussoc, or mumble's sso client) during qa.

To post a comment you must log in.
Revision history for this message
Anthony Lenton (elachuni) wrote :

Youch, apologies for the huge diff. Fwiw, the wadl template was taken verbatim from what lazr.restful generated previous to being removed, so that's ~1.5k lines that shouldn't need much review

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

How do you imagine fixing API in the future taking into the account that from now on the WADL file will have to be modified by hand? Or is this API set in stone and no signature will ever change (of course, besides moving to new version)?

review: Needs Information
Revision history for this message
Anthony Lenton (elachuni) wrote :

> How do you imagine fixing API in the future taking into the account that from
> now on the WADL file will have to be modified by hand? Or is this API set in
> stone and no signature will ever change (of course, besides moving to new
> version)?

Yup, next branch should add a 2.0 api (in real Piston style) that can be more easily extended in the future.

Revision history for this message
Anthony Lenton (elachuni) wrote :

Merged in latest changes for trunk. Requesting a new review.

Revision history for this message
ISD Branch Mangler (isd-branches-mangler) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2011-03-07 22:20:21 +0000
3+++ debian/control 2011-05-24 16:31:52 +0000
4@@ -20,15 +20,6 @@
5 python-django-oauth-backend (= 0.1.dev1),
6 python-django-preflight (>= 0.1),
7 python-django-preflight (<< 0.2),
8- python-django-settings (>= 0.2),
9 python-memcache,
10- python-lazr.restful (>= 0.9.18),
11- python-lazr.authentication,
12- python-docutils,
13- python-epydoc,
14- python-restrictedpython,
15- python-schemaconfig (>= 0.1.3.dev1),
16- python-simplejson,
17- python-lazr.uri
18-Conflicts: python-lazr.restful (>= 0.9.22)
19+ python-simplejson
20 Description: Canonical OpenID provider
21
22=== modified file 'django_project/config.example/settings.py'
23--- django_project/config.example/settings.py 2010-08-04 22:13:07 +0000
24+++ django_project/config.example/settings.py 2011-05-24 16:31:52 +0000
25@@ -41,17 +41,11 @@
26 # api settings
27 API_ENABLED = False
28 API_HOST = 'openid.localdomain'
29+OAUTH_DATA_STORE = 'identityprovider.store.SSODataStore'
30 APP_SERVERS = [{'SERVER_ID': 'localhost',
31 'HOST': 'localhost',
32 'PORT': 8000,
33 }]
34-LAZR_RESTFUL_CODE_REVISION = '1'
35-LAZR_RESTFUL_HOSTNAME = 'sso'
36-LAZR_RESTFUL_PATH_OVERRIDE = None
37-LAZR_RESTFUL_SERVICE_ROOT_URI_PREFIX = 'api/'
38-LAZR_RESTFUL_SERVICE_VERSION_URI_PREFIX = '1.0'
39-LAZR_RESTFUL_USE_HTTPS = False
40-LAZR_RESTFUL_VIEW_PERMISSION = 'zope.Public'
41
42 # branding settings
43 BRANDED_TEMPLATE_DIR = 'ubuntu'
44
45=== modified file 'doctests/stories/api-authentications.txt'
46--- doctests/stories/api-authentications.txt 2011-01-24 18:28:24 +0000
47+++ doctests/stories/api-authentications.txt 2011-05-24 16:31:52 +0000
48@@ -120,19 +120,19 @@
49 >>> api.authentications.list_tokens(consumer_key='myopenid')
50 Traceback (most recent call last):
51 ...
52- HTTPError: HTTP Error 403: Forbidden
53+ HTTPError: HTTP Error 403: FORBIDDEN
54 ...
55 >>> api.authentications.invalidate_token(consumer_key='myopenid',
56 ... token='mytoken')
57 Traceback (most recent call last):
58 ...
59- HTTPError: HTTP Error 403: Forbidden
60+ HTTPError: HTTP Error 403: FORBIDDEN
61 ...
62 >>> api.authentications.validate_token(consumer_key='myopenid',
63 ... token='mytoken')
64 Traceback (most recent call last):
65 ...
66- HTTPError: HTTP Error 403: Forbidden
67+ HTTPError: HTTP Error 403: FORBIDDEN
68 ...
69
70 Test When OAuth Auth Credentials For Normal Users Are Supplied
71@@ -218,5 +218,5 @@
72 >>> api.authentications.authenticate(token_name='this-machine')
73 Traceback (most recent call last):
74 ...
75- HTTPError: HTTP Error 403: Forbidden
76+ HTTPError: HTTP Error 403: FORBIDDEN
77 ...
78
79=== modified file 'doctests/stories/api-workflows.txt'
80--- doctests/stories/api-workflows.txt 2011-03-14 15:56:30 +0000
81+++ doctests/stories/api-workflows.txt 2011-05-24 16:31:52 +0000
82@@ -46,8 +46,11 @@
83 the human being:
84
85 >>> captcha = api.captchas.new()
86- >>> captcha
87- {u'captcha_id': u'...', u'image_url': u'https://api-secure.recaptcha.net/image?c=...'}
88+ >>> sorted(captcha.keys())
89+ [u'captcha_id', u'image_url']
90+ >>> prefix = u'https://api-secure.recaptcha.net/image?c='
91+ >>> captcha['image_url'].startswith(prefix)
92+ True
93
94 As you can see captcha is dictionary with two fields: ``captcha_id``
95 which you need to pass to registration function and ``image_url`` which
96
97=== added directory 'identityprovider/api10'
98=== added file 'identityprovider/api10/__init__.py'
99=== added file 'identityprovider/api10/decorators.py'
100--- identityprovider/api10/decorators.py 1970-01-01 00:00:00 +0000
101+++ identityprovider/api10/decorators.py 2011-05-24 16:31:52 +0000
102@@ -0,0 +1,36 @@
103+# Copyright 2010 Canonical Ltd. This software is licensed under the
104+# GNU Affero General Public License version 3 (see the file LICENSE).
105+
106+from functools import wraps
107+
108+from django.http import HttpResponseForbidden
109+
110+from identityprovider.models import (
111+ Account,
112+ APIUser,
113+)
114+
115+def api_user_required(func):
116+ @wraps(func)
117+ def wrapper(self, request, *args, **kwargs):
118+ user = request.user
119+ if user:
120+ if isinstance(user, APIUser):
121+ return func(self, request, *args, **kwargs)
122+ return HttpResponseForbidden('403 Forbidden')
123+ return wrapper
124+
125+
126+def plain_user_required(func):
127+ @wraps(func)
128+ def wrapper(self, request, *args, **kwargs):
129+ user = request.user
130+ if user:
131+ if isinstance(user, Account):
132+ return func(self, request, *args, **kwargs)
133+ return HttpResponseForbidden('403 Forbidden')
134+ return wrapper
135+
136+def named_operation(func):
137+ func.is_named_operation = True
138+ return func
139
140=== added file 'identityprovider/api10/forms.py'
141--- identityprovider/api10/forms.py 1970-01-01 00:00:00 +0000
142+++ identityprovider/api10/forms.py 2011-05-24 16:31:52 +0000
143@@ -0,0 +1,55 @@
144+# Copyright 2010 Canonical Ltd. This software is licensed under the
145+# GNU Affero General Public License version 3 (see the file LICENSE).
146+
147+# We use Django forms for webservice input validation
148+
149+from django import forms
150+from django.forms import fields
151+from django.utils.translation import ugettext as _
152+
153+from identityprovider.utils import password_policy_compliant
154+from identityprovider.models.captcha import Captcha, VerifyCaptchaError
155+
156+
157+PASSWORD_POLICY_ERROR = _("Password must be at least "
158+ "8 characters long, and must contain at least one "
159+ "number and an upper case letter.")
160+
161+
162+class WebserviceCreateAccountForm(forms.Form):
163+ email = fields.EmailField()
164+ password = fields.CharField(max_length=256)
165+ captcha_id = fields.CharField(max_length=1024)
166+ captcha_solution = fields.CharField(max_length=256)
167+ remote_ip = fields.CharField(max_length=256)
168+ displayname = fields.CharField(max_length=256, required=False)
169+
170+ def clean_password(self):
171+ if 'password' in self.cleaned_data:
172+ password = self.cleaned_data['password']
173+ try:
174+ str(password)
175+ except UnicodeEncodeError:
176+ raise forms.ValidationError(
177+ _("Invalid characters in password"))
178+ if not password_policy_compliant(password):
179+ raise forms.ValidationError(PASSWORD_POLICY_ERROR)
180+ return password
181+
182+ def clean(self):
183+ cleaned_data = self.cleaned_data
184+ captcha_id = cleaned_data.get('captcha_id')
185+ captcha_solution = cleaned_data.get('captcha_solution')
186+
187+ # The remote IP address is absolutely required, and comes from
188+ # SSO itself, not from the client. If it's missing, it's a
189+ # programming error, and should not be returned to the client
190+ # as a validation error. So, we use a normal key lookup here.
191+ remote_ip = cleaned_data['remote_ip']
192+
193+ captcha = Captcha(captcha_id)
194+
195+ if captcha.verify(captcha_solution, remote_ip):
196+ return cleaned_data
197+ # not verified
198+ raise forms.ValidationError(_("Wrong captcha solution."))
199
200=== added file 'identityprovider/api10/handlers.py'
201--- identityprovider/api10/handlers.py 1970-01-01 00:00:00 +0000
202+++ identityprovider/api10/handlers.py 2011-05-24 16:31:52 +0000
203@@ -0,0 +1,410 @@
204+# Copyright 2010 Canonical Ltd. This software is licensed under the
205+# GNU Affero General Public License version 3 (see the file LICENSE).
206+
207+from django.conf import settings
208+from django.core.serializers.json import DateTimeAwareJSONEncoder
209+from django.http import HttpResponseBadRequest
210+from django.shortcuts import render_to_response
211+from django.utils import simplejson
212+from piston.emitters import Emitter
213+from piston.handler import BaseHandler
214+
215+from identityprovider.api10.decorators import (
216+ api_user_required,
217+ plain_user_required,
218+ named_operation,
219+)
220+from identityprovider.api10.forms import (
221+ PASSWORD_POLICY_ERROR,
222+ WebserviceCreateAccountForm,
223+)
224+from identityprovider.models import (
225+ EmailAddress,
226+ Account,
227+ AccountPassword,
228+ AuthToken,
229+ AuthTokenFactory,
230+)
231+from identityprovider.models.const import (
232+ AccountCreationRationale,
233+ AccountStatus,
234+ EmailStatus,
235+ LoginTokenType,
236+)
237+from identityprovider.models.captcha import (
238+ Captcha,
239+ NewCaptchaError,
240+)
241+from identityprovider.views.server import get_team_memberships
242+from identityprovider.signals import (
243+ account_created,
244+ account_email_validated,
245+ application_token_created,
246+ application_token_invalidated,
247+)
248+from identityprovider.utils import (
249+ CannotResetPasswordException,
250+ encrypt_launchpad_password,
251+ get_person_and_account_by_email,
252+ password_policy_compliant,
253+ PersonAndAccountNotFoundException,
254+)
255+from oauth_backend.models import Token
256+
257+
258+class CanNotResetPasswordError(Exception):
259+ pass
260+
261+
262+class LazrRestfulEmitter(Emitter):
263+ """JSON emitter, lazr.restful flavoured.
264+
265+ Reports content-type 'application/json', without specifying a charset
266+ as that seems to confuse lazr.restfulclient.
267+ """
268+ def render(self, request):
269+ seria = simplejson.dumps(self.construct(),
270+ cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
271+
272+ return seria
273+Emitter.register('lazr.restful', LazrRestfulEmitter, 'application/json')
274+
275+class LazrRestfulHandler(BaseHandler):
276+ allowed_methods = ('GET', 'POST')
277+
278+ response = {}
279+
280+ baseurl = settings.SSO_ROOT_URL.strip('/')
281+
282+ def __init__(self):
283+ self.response = self.response.copy()
284+ for key, value in self.response.items():
285+ if key.endswith('_link'):
286+ self.response[key] = value % self.baseurl
287+
288+ def read(self, request):
289+ if not 'ws.op' in request.GET:
290+ return self.response
291+ return self.named_operation(request, request.GET)
292+
293+ def create(self, request):
294+ if not 'ws.op' in request.POST:
295+ return HttpResponseBadRequest('No operation name given.')
296+ return self.named_operation(request, request.POST)
297+
298+ def named_operation(self, request, serialized):
299+ method_name = serialized['ws.op']
300+ method = getattr(self, method_name, None)
301+ is_named_operation = getattr(method, 'is_named_operation', False)
302+ if not is_named_operation:
303+ return HttpResponseBadRequest('No such operation: %s' %
304+ method_name)
305+ request.data = self.lazr_restful_deserialize(serialized)
306+ return method(request)
307+
308+ def lazr_restful_deserialize(self, serialized):
309+ data = {}
310+ for key in serialized:
311+ if key == 'ws.op':
312+ continue
313+ data[key] = simplejson.loads(serialized[key])
314+ return data
315+
316+
317+class RootHandler(LazrRestfulHandler):
318+ allowed_methods = ('GET',)
319+
320+ response = {
321+ "registrations_collection_link": "%s/api/1.0/registration",
322+ "captchas_collection_link": "%s/api/1.0/captchas",
323+ "validations_collection_link": "%s/api/1.0/validation",
324+ "authentications_collection_link": "%s/api/1.0/authentications",
325+ "resource_type_link": "%s/api/1.0/#service-root",
326+ "accounts_collection_link": "%s/api/1.0/accounts",
327+ }
328+
329+ def read(self, request):
330+ if ('application/vd.sun.wadl+xml' in request.META['HTTP_ACCEPT'] or
331+ 'application/vnd.sun.wadl+xml' in request.META['HTTP_ACCEPT']):
332+ context = {'baseurl': self.baseurl}
333+ return render_to_response('wadl1.0.xml', context)
334+ else:
335+ return self.response
336+
337+class CaptchaHandler(LazrRestfulHandler):
338+ response = {
339+ "total_size": 0,
340+ "start": None,
341+ "resource_type_link": "%s/api/1.0/#captchas",
342+ "entries": []
343+ }
344+
345+ @named_operation
346+ def new(self, request):
347+ try:
348+ return Captcha.new().serialize()
349+ except NewCaptchaError, e:
350+ request.environ['oops-dump'] = True
351+ logging.warning(e.traceback)
352+ logging.warning("Failed to connect to reCaptcha server")
353+ # TODO: Some better return here?
354+ return e.dummy
355+
356+
357+class RegistrationHandler(LazrRestfulHandler):
358+ response = {
359+ "total_size": 0,
360+ "start": None,
361+ "resource_type_link": "%s/api/1.0/#registrations",
362+ "entries": []
363+ }
364+
365+ @named_operation
366+ def register(self, request):
367+ data = request.data
368+ data['remote_ip'] = request.environ['REMOTE_ADDR']
369+ form = WebserviceCreateAccountForm(data)
370+ try:
371+ if not form.is_valid():
372+ errors = dict((k, map(unicode, v))
373+ for (k, v) in form.errors.items())
374+ result = {'status': 'error', 'errors': errors}
375+ return result
376+ except VerifyCaptchaError, e:
377+ request.environment['oops-dump'] = True
378+ logging.warning(e.traceback)
379+ logging.warning("reCaptcha connection error")
380+ return {'status': 'error', 'errors':
381+ {'captcha_solution': [
382+ 'Unable to verify captcha. Please try again shortly.']}}
383+
384+ cleaned_data = form.cleaned_data
385+ requested_email = cleaned_data['email']
386+ emails = EmailAddress.objects.filter(email__iexact=requested_email)
387+ if len(emails) > 0:
388+ return {'status': 'error', 'errors':
389+ {'email': ['Email already registered']}}
390+
391+ account = Account.objects.create(
392+ creation_rationale=AccountCreationRationale.OWNER_CREATED_LAUNCHPAD,
393+ status=AccountStatus.ACTIVE,
394+ displayname=cleaned_data['displayname'])
395+
396+ account.emailaddress_set.create(
397+ email=cleaned_data['email'],
398+ status=EmailStatus.NEW)
399+
400+ AccountPassword.objects.create(
401+ password=encrypt_launchpad_password(cleaned_data['password']),
402+ account=account)
403+
404+ token = AuthTokenFactory().new_api_email_validation_token(
405+ account, cleaned_data['email'])
406+
407+ token.sendNewUserEmail('api-newuser.txt')
408+
409+ account_created.send(sender=self,
410+ openid_identifier=account.openid_identifier)
411+
412+ return {
413+ 'status': 'ok',
414+ 'message': "Email verification required."
415+ }
416+
417+ @named_operation
418+ def request_password_reset_token(self, request):
419+ data = request.data
420+ email = data['email']
421+ error = False
422+ try:
423+ person, account = get_person_and_account_by_email(email)
424+ except (CannotResetPasswordException,
425+ PersonAndAccountNotFoundException):
426+ error = True
427+
428+ if error or (account is not None and not account.can_reset_password):
429+ raise CanNotResetPasswordError(
430+ "Can't reset password for this account")
431+
432+ token = AuthTokenFactory().new(account, email, email,
433+ LoginTokenType.PASSWORDRECOVERY, None)
434+
435+ token.sendPasswordResetEmail()
436+
437+ return {
438+ 'status': 'ok',
439+ 'message': "Password reset token sent."
440+ }
441+
442+ @named_operation
443+ def set_new_password(self, request):
444+ data = request.data
445+ new_password = data['new_password']
446+ token = AuthToken.objects.get(
447+ email=data['email'], token=data['token'],
448+ token_type=LoginTokenType.PASSWORDRECOVERY)
449+ if not token.requester:
450+ token.delete()
451+ return {
452+ 'status': 'error',
453+ 'message': "Wrong token, request new one."
454+ }
455+ if not password_policy_compliant(new_password):
456+ return {
457+ 'status': 'error',
458+ 'errors': [PASSWORD_POLICY_ERROR]
459+ }
460+ password_obj = token.requester.accountpassword
461+ password_obj.password = encrypt_launchpad_password(new_password)
462+ password_obj.save()
463+
464+ token.consume()
465+
466+ return {
467+ 'status': 'ok',
468+ 'message': "Password changed"
469+ }
470+
471+
472+def _serialize_account(user):
473+ emails = EmailAddress.objects.filter(account=user,
474+ status=EmailStatus.VALIDATED)
475+ preferred_email = user.preferredemail
476+ if preferred_email is not None:
477+ preferred_email = preferred_email.email
478+
479+ if user.person:
480+ username = user.person.name
481+ else:
482+ username = user.openid_identifier
483+
484+ return {
485+ 'username': username,
486+ 'displayname': user.displayname,
487+ 'openid_identifier': user.openid_identifier,
488+ 'preferred_email': preferred_email,
489+ 'verified_emails': [e.email for e in emails],
490+ 'unverified_emails': [e.email for e in user.unverified_emails()],
491+ }
492+
493+
494+class AuthenticationHandler(LazrRestfulHandler):
495+ """ All these methods assume that they're run behind Basic Auth """
496+ response = {
497+ "total_size": 0,
498+ "start": None,
499+ "resource_type_link": "%s/api/1.0/#authentications",
500+ "entries": []
501+ }
502+
503+ @plain_user_required
504+ @named_operation
505+ def authenticate(self, request):
506+ data = request.data
507+ account = request.user
508+ token = account.create_oauth_token(data['token_name'])
509+ application_token_created.send(
510+ sender=self, openid_identifier=account.openid_identifier)
511+ return token.serialize()
512+
513+ @api_user_required
514+ @named_operation
515+ def list_tokens(self, request):
516+ data = request.data
517+ tokens = Token.objects.filter(
518+ consumer__user__username=data['consumer_key'])
519+ result = [{'token': t.token, 'name': t.name} for t in tokens]
520+ return result
521+
522+ @api_user_required
523+ @named_operation
524+ def validate_token(self, request):
525+ data = request.data
526+ try:
527+ token = Token.objects.get(
528+ consumer__user__username=data['consumer_key'],
529+ token=data['token'])
530+ return token.serialize()
531+ except Token.DoesNotExist:
532+ return False
533+
534+ @api_user_required
535+ @named_operation
536+ def invalidate_token(self, request):
537+ data = request.data
538+ tokens = Token.objects.filter(token=data['token'],
539+ consumer__user__username=data['consumer_key'])
540+ tokens.delete()
541+ application_token_invalidated.send(
542+ sender=self, openid_identifier=data['consumer_key'])
543+
544+
545+ @api_user_required
546+ @named_operation
547+ def team_memberships(self, request):
548+ data = request.data
549+ accounts = Account.objects.filter(
550+ openid_identifier=data['openid_identifier'])
551+ accounts = list(accounts)
552+
553+ if len(accounts) == 1:
554+ account = accounts[0]
555+ memberships = get_team_memberships(data['team_names'],
556+ account, False)
557+ return memberships
558+ else:
559+ return []
560+
561+ @api_user_required
562+ @named_operation
563+ def account_by_email(self, request):
564+ data = request.data
565+ account = Account.objects.get_by_email(data['email'])
566+ if account:
567+ return _serialize_account(account)
568+ else:
569+ return None
570+
571+ @api_user_required
572+ @named_operation
573+ def account_by_openid(self, request):
574+ data = request.data
575+ try:
576+ account = Account.objects.get(openid_identifier=data['openid'])
577+ except Account.DoesNotExist:
578+ return None
579+ else:
580+ return _serialize_account(account)
581+
582+
583+class AccountsHandler(LazrRestfulHandler):
584+ @named_operation
585+ def me(self, request):
586+ account = request.user
587+ return _serialize_account(account)
588+
589+ @named_operation
590+ def team_memberships(self, request):
591+ team_names = request.data['team_names']
592+ memberships = get_team_memberships(team_names, request.user, True)
593+ return memberships
594+
595+ @named_operation
596+ def validate_email(self, request):
597+ email_token = request.data['email_token']
598+ try:
599+ token = request.user.authtoken_set.get(
600+ token=email_token, token_type=LoginTokenType.VALIDATEEMAIL)
601+
602+ email = EmailAddress.objects.get(email__iexact=token.email)
603+ email.status = EmailStatus.VALIDATED
604+ email.save()
605+
606+ token.consume()
607+
608+ account_email_validated.send(
609+ openid_identifier=request.user.openid_identifier,
610+ sender=self)
611+ return {'email': email.email}
612+ except AuthToken.DoesNotExist:
613+ return {'errors': {'email_token': ["Bad email token!"]}}
614
615=== added file 'identityprovider/api10/urls.py'
616--- identityprovider/api10/urls.py 1970-01-01 00:00:00 +0000
617+++ identityprovider/api10/urls.py 2011-05-24 16:31:52 +0000
618@@ -0,0 +1,41 @@
619+# Copyright 2010 Canonical Ltd. This software is licensed under the
620+# GNU Affero General Public License version 3 (see the file LICENSE).
621+
622+from django.conf.urls.defaults import patterns, url
623+
624+from piston.authentication import HttpBasicAuthentication
625+from piston.resource import Resource
626+
627+from identityprovider.api10.handlers import (
628+ AccountsHandler,
629+ AuthenticationHandler,
630+ CaptchaHandler,
631+ RegistrationHandler,
632+ RootHandler,
633+)
634+from identityprovider.auth import (
635+ basic_authenticate,
636+ SSOOAuthAuthentication,
637+)
638+
639+root_resource = Resource(handler=RootHandler)
640+captcha_resource = Resource(handler=CaptchaHandler)
641+registration_resource = Resource(handler=RegistrationHandler)
642+authentication_resource = Resource(handler=AuthenticationHandler,
643+ authentication=HttpBasicAuthentication(auth_func=basic_authenticate))
644+accounts_resource = Resource(handler=AccountsHandler,
645+ authentication=SSOOAuthAuthentication())
646+
647+urlpatterns = patterns('',
648+ url(r'^$', root_resource,
649+ kwargs={'emitter_format': 'lazr.restful'}),
650+ url(r'^captchas$', captcha_resource,
651+ kwargs={'emitter_format': 'lazr.restful'}),
652+ url(r'^registration$', registration_resource,
653+ kwargs={'emitter_format': 'lazr.restful'}),
654+ url(r'^authentications$', authentication_resource,
655+ kwargs={'emitter_format': 'lazr.restful'}),
656+ url(r'^accounts$', accounts_resource,
657+ kwargs={'emitter_format': 'lazr.restful'}),
658+)
659+
660
661=== modified file 'identityprovider/auth.py'
662--- identityprovider/auth.py 2011-03-04 17:09:45 +0000
663+++ identityprovider/auth.py 2011-05-24 16:31:52 +0000
664@@ -2,9 +2,11 @@
665 # GNU Affero General Public License version 3 (see the file LICENSE).
666
667 from datetime import datetime
668+from django.http import HttpResponse
669
670 from django.conf import settings
671 from oauth_backend.models import Consumer, Token
672+from piston.authentication import OAuthAuthentication
673
674 from identityprovider.models import Account, AccountPassword, EmailAddress
675 from identityprovider.models.api import APIUser
676@@ -66,6 +68,30 @@
677 return user
678
679
680+class SSOOAuthAuthentication(OAuthAuthentication):
681+ def is_authenticated(self, request):
682+ if not self.is_valid_request(request):
683+ return False
684+ try:
685+ consumer, token, parameters = self.validate_token(request)
686+ except oauth.OAuthError, err:
687+ return False
688+ if not (consumer and token):
689+ return False
690+ # only allow authentication if account is active
691+ account = Account.objects.get(openid_identifier=consumer.key)
692+ if not account.is_active:
693+ return False
694+ request.user = account
695+ request.throttle_extra = token.consumer.id
696+ return True
697+
698+ def challenge(self):
699+ resp = HttpResponse("Authorization Required")
700+ resp['WWW-Authenticate'] = 'OAuth realm="%s"' % self.realm
701+ resp.status_code = 401
702+ return resp
703+
704 def oauth_authenticate(oauth_consumer, oauth_token, parameters):
705 """Currently only checks that given consumer and token are in database"""
706 try:
707
708=== modified file 'identityprovider/models/account.py'
709--- identityprovider/models/account.py 2011-05-11 15:29:56 +0000
710+++ identityprovider/models/account.py 2011-05-24 16:31:52 +0000
711@@ -8,7 +8,6 @@
712 from django.contrib.auth.models import User
713 from django.db import models
714 from django.utils.translation import ugettext_lazy as _
715-from zope.interface import classImplements
716
717 from oauth_backend.models import Consumer
718
719@@ -18,7 +17,6 @@
720 encrypt_launchpad_password,
721 generate_openid_identifier,
722 is_django_13)
723-from identityprovider.webservice.interfaces import IAccount
724
725 __all__ = (
726 'Account',
727@@ -293,18 +291,6 @@
728 # now go and effectively save it
729 super(Account, self).save(force_insert, force_update, **kwargs)
730
731- # IDjangoLocation implementation
732- @property
733- def __parent__(self):
734- from identityprovider.webservice.models import AccountSet
735- return AccountSet()
736-
737- @property
738- def __url_path__(self):
739- return str(self.id)
740-
741-classImplements(Account, IAccount)
742-
743
744 class AccountPassword(models.Model):
745 account = models.OneToOneField(Account, db_column='account')
746
747=== modified file 'identityprovider/preflight.py'
748--- identityprovider/preflight.py 2011-03-07 23:22:00 +0000
749+++ identityprovider/preflight.py 2011-05-24 16:31:52 +0000
750@@ -21,13 +21,13 @@
751 return internal_authorize(request.user)
752
753 def versions(self):
754- import lazr
755 import openid
756 import identityprovider
757+ import piston.utils
758 return [
759 {'name': 'SSO', 'version': identityprovider.__version__},
760- {'name': 'lazr.restful', 'version': lazr.restful.__version__},
761 {'name': 'openid', 'version': openid.__version__},
762+ {'name': 'piston', 'version': piston.utils.get_version()},
763 ]
764
765
766
767=== added file 'identityprovider/store.py'
768--- identityprovider/store.py 1970-01-01 00:00:00 +0000
769+++ identityprovider/store.py 2011-05-24 16:31:52 +0000
770@@ -0,0 +1,35 @@
771+from oauth.oauth import OAuthToken
772+from oauth_backend.models import Token, DataStore
773+
774+
775+class SSODataStore(DataStore):
776+ def __init__(self, oauth_request=None):
777+ """To serve as a Piston datastore we'll need provide this signature.
778+
779+ We later won't use the oauth_request, so we can ignore it here.
780+ """
781+ super(SSODataStore, self).__init__()
782+
783+ def lookup_token(self, token_type, token_field):
784+ """
785+ :param token_type: type of token to lookup
786+ :param token_field: token to look up
787+
788+ :note: token_type should always be 'access' as only such tokens are
789+ stored in database
790+
791+ :returns: OAuthToken object
792+ """
793+ assert token_type == 'access'
794+
795+ try:
796+ token = Token.objects.get(token=token_field)
797+ # Piston expects OAuth tokens to have 'consumer' and 'user' atts.
798+ # (see piston.authentication.OAuthAuthentication.is_authenticated)
799+ oauthtoken = OAuthToken(token.token, token.token_secret)
800+ oauthtoken.consumer = token.consumer
801+ oauthtoken.user = token.consumer.user
802+ return oauthtoken
803+ except Token.DoesNotExist:
804+ return None
805+
806
807=== added file 'identityprovider/templates/wadl1.0.xml'
808--- identityprovider/templates/wadl1.0.xml 1970-01-01 00:00:00 +0000
809+++ identityprovider/templates/wadl1.0.xml 2011-05-24 16:31:52 +0000
810@@ -0,0 +1,1378 @@
811+<?xml version="1.0"?>
812+<!DOCTYPE application [
813+ <!ENTITY nbsp "\&#160;">
814+]>
815+<wadl:application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
816+ xmlns="http://research.sun.com/wadl/2006/10"
817+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
818+ xmlns:wadl="http://research.sun.com/wadl/2006/10"
819+ xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd">
820+
821+ <!--There is one "service root" resource, located (as you'd expect)
822+ at the service root. This very document is the WADL
823+ representation of the "service root" resource.-->
824+ <wadl:resources base="{{ baseurl }}/api/1.0/">
825+ <wadl:resource path="" type="#service-root"/>
826+ </wadl:resources>
827+
828+ <!--A "service root" resource responds to GET.-->
829+ <wadl:resource_type id="service-root">
830+ <wadl:doc>The root of the web service.</wadl:doc>
831+ <wadl:method name="GET" id="service-root-get">
832+ <wadl:response>
833+ <wadl:representation href="#service-root-json"/>
834+ <wadl:representation mediaType="application/vnd.sun.wadl+xml" id="service-root-wadl"/>
835+ </wadl:response>
836+ </wadl:method>
837+ </wadl:resource_type>
838+
839+ <!--The JSON representation of a "service root" resource contains a
840+ number of links to collection-type resources.-->
841+ <wadl:representation mediaType="application/json" id="service-root-json">
842+
843+ <wadl:param style="plain"
844+ path="$['registrations_collection_link']"
845+ name="registrations_collection_link">
846+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#registrations"/>
847+ </wadl:param>
848+
849+
850+ <wadl:param style="plain"
851+ path="$['authentications_collection_link']"
852+ name="authentications_collection_link">
853+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#authentications"/>
854+ </wadl:param>
855+
856+
857+ <wadl:param style="plain"
858+ path="$['captchas_collection_link']"
859+ name="captchas_collection_link">
860+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#captchas"/>
861+ </wadl:param>
862+
863+
864+ <wadl:param style="plain"
865+ path="$['accounts_collection_link']"
866+ name="accounts_collection_link">
867+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#accounts"/>
868+ </wadl:param>
869+
870+
871+ <wadl:param style="plain"
872+ path="$['validations_collection_link']"
873+ name="validations_collection_link">
874+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#accounts"/>
875+ </wadl:param>
876+
877+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
878+ <wadl:doc>The link to the WADL description of this resource.</wadl:doc>
879+ <wadl:link/>
880+ </wadl:param>
881+ </wadl:representation>
882+
883+ <!--In addition to the service root, this document describes all the
884+ types of resources you might encounter as you browse this web
885+ service.-->
886+
887+ <!--Begin resource_type definitions for collection resources.-->
888+
889+ <wadl:resource_type id="accounts">
890+
891+ <wadl:method name="GET" id="accounts-get">
892+ <wadl:response>
893+ <wadl:representation
894+ href="{{ baseurl }}/api/1.0/#account-page"/>
895+ <wadl:representation
896+ mediaType="application/vnd.sun.wadl+xml"
897+ id="accounts-wadl"/>
898+ </wadl:response>
899+ </wadl:method>
900+
901+ <wadl:method id="accounts-me" name="GET">
902+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
903+Get details for the currently authenticated user.
904+</wadl:doc>
905+ <wadl:request>
906+
907+ <wadl:param style="query" name="ws.op"
908+ required="true" fixed="me">
909+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
910+ </wadl:param>
911+
912+ </wadl:request>
913+
914+ </wadl:method>
915+ <wadl:method id="accounts-team_memberships" name="GET">
916+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
917+Query account for team memberships
918+</wadl:doc>
919+ <wadl:request>
920+
921+ <wadl:param style="query" name="ws.op"
922+ required="true"
923+ fixed="team_memberships">
924+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
925+ </wadl:param>
926+ <wadl:param style="query" required="true"
927+ name="team_names">
928+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
929+List of team names to check
930+</wadl:doc>
931+
932+ </wadl:param>
933+
934+ </wadl:request>
935+
936+ </wadl:method>
937+ <wadl:method id="accounts-validate_email" name="GET">
938+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
939+Validate email by sending token user received in email
940+</wadl:doc>
941+ <wadl:request>
942+
943+ <wadl:param style="query" name="ws.op"
944+ required="true"
945+ fixed="validate_email">
946+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
947+ </wadl:param>
948+ <wadl:param style="query" required="true"
949+ name="email_token">
950+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
951+Email validation token.
952+</wadl:doc>
953+
954+ </wadl:param>
955+
956+ </wadl:request>
957+
958+ </wadl:method>
959+ </wadl:resource_type>
960+
961+
962+
963+ <wadl:resource_type id="authentications">
964+
965+ <wadl:method name="GET" id="authentications-get">
966+ <wadl:response>
967+ <wadl:representation
968+ href="{{ baseurl }}/api/1.0/#authentication-page"/>
969+ <wadl:representation
970+ mediaType="application/vnd.sun.wadl+xml"
971+ id="authentications-wadl"/>
972+ </wadl:response>
973+ </wadl:method>
974+
975+ <wadl:method id="authentications-me" name="GET">
976+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
977+Get details for the currently authenticated user.
978+</wadl:doc>
979+ <wadl:request>
980+
981+ <wadl:param style="query" name="ws.op"
982+ required="true" fixed="me">
983+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
984+ </wadl:param>
985+
986+ </wadl:request>
987+
988+ </wadl:method>
989+ <wadl:method id="authentications-authenticate"
990+ name="GET">
991+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
992+Obtain OAuth token for logged in user
993+</wadl:doc>
994+ <wadl:request>
995+
996+ <wadl:param style="query" name="ws.op"
997+ required="true" fixed="authenticate">
998+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
999+ </wadl:param>
1000+ <wadl:param style="query" required="true"
1001+ name="token_name">
1002+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1003+Token name.
1004+</wadl:doc>
1005+
1006+ </wadl:param>
1007+
1008+ </wadl:request>
1009+
1010+ </wadl:method>
1011+ <wadl:method id="authentications-team_memberships"
1012+ name="GET">
1013+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1014+Query user for team memberships
1015+</wadl:doc>
1016+ <wadl:request>
1017+
1018+ <wadl:param style="query" name="ws.op"
1019+ required="true"
1020+ fixed="team_memberships">
1021+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1022+ </wadl:param>
1023+ <wadl:param style="query" required="true"
1024+ name="team_names">
1025+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1026+List of team names to check
1027+</wadl:doc>
1028+
1029+ </wadl:param>
1030+ <wadl:param style="query" required="true"
1031+ name="openid_identifier">
1032+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1033+OpenID Identifier used for asking forteam memberships on behalf of other user.
1034+</wadl:doc>
1035+
1036+ </wadl:param>
1037+
1038+ </wadl:request>
1039+
1040+ </wadl:method>
1041+ <wadl:method id="authentications-validate_token"
1042+ name="GET">
1043+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1044+<p>Check that a token is valid.</p>
1045+<p>If valid, this method return the token and consumer secrets</p>
1046+
1047+</wadl:doc>
1048+ <wadl:request>
1049+
1050+ <wadl:param style="query" name="ws.op"
1051+ required="true"
1052+ fixed="validate_token">
1053+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1054+ </wadl:param>
1055+ <wadl:param style="query" required="true"
1056+ name="token">
1057+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1058+The token you want to validate
1059+</wadl:doc>
1060+
1061+ </wadl:param>
1062+ <wadl:param style="query" required="true"
1063+ name="consumer_key">
1064+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1065+The consumer key (openid identifier)
1066+</wadl:doc>
1067+
1068+ </wadl:param>
1069+
1070+ </wadl:request>
1071+
1072+ </wadl:method>
1073+ <wadl:method id="authentications-account_by_openid"
1074+ name="GET">
1075+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1076+Retrieve account information based on OpenID identifier
1077+</wadl:doc>
1078+ <wadl:request>
1079+
1080+ <wadl:param style="query" name="ws.op"
1081+ required="true"
1082+ fixed="account_by_openid">
1083+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1084+ </wadl:param>
1085+ <wadl:param style="query" required="true"
1086+ name="openid">
1087+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1088+OpenID Identifier of the account
1089+</wadl:doc>
1090+
1091+ </wadl:param>
1092+
1093+ </wadl:request>
1094+
1095+ </wadl:method>
1096+ <wadl:method id="authentications-validate_email"
1097+ name="GET">
1098+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1099+Validate email by sending token user received in email
1100+</wadl:doc>
1101+ <wadl:request>
1102+
1103+ <wadl:param style="query" name="ws.op"
1104+ required="true"
1105+ fixed="validate_email">
1106+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1107+ </wadl:param>
1108+ <wadl:param style="query" required="true"
1109+ name="email_token">
1110+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1111+Email validation token.
1112+</wadl:doc>
1113+
1114+ </wadl:param>
1115+
1116+ </wadl:request>
1117+
1118+ </wadl:method>
1119+ <wadl:method id="authentications-account_by_email"
1120+ name="GET">
1121+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1122+Retrieve account information based on email address
1123+</wadl:doc>
1124+ <wadl:request>
1125+
1126+ <wadl:param style="query" name="ws.op"
1127+ required="true"
1128+ fixed="account_by_email">
1129+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1130+ </wadl:param>
1131+ <wadl:param style="query" required="true"
1132+ name="email">
1133+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1134+Email address of the account
1135+</wadl:doc>
1136+
1137+ </wadl:param>
1138+
1139+ </wadl:request>
1140+
1141+ </wadl:method>
1142+ <wadl:method id="authentications-list_tokens"
1143+ name="GET">
1144+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1145+Get the currently valid tokens for a given user
1146+</wadl:doc>
1147+ <wadl:request>
1148+
1149+ <wadl:param style="query" name="ws.op"
1150+ required="true" fixed="list_tokens">
1151+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1152+ </wadl:param>
1153+ <wadl:param style="query" required="true"
1154+ name="consumer_key">
1155+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1156+User's OpenID identifier
1157+</wadl:doc>
1158+
1159+ </wadl:param>
1160+
1161+ </wadl:request>
1162+
1163+ </wadl:method>
1164+ <wadl:method id="authentications-invalidate_token"
1165+ name="POST">
1166+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1167+Make the given token invalid
1168+</wadl:doc>
1169+ <wadl:request>
1170+ <wadl:representation
1171+ mediaType="application/x-www-form-urlencoded">
1172+ <wadl:param style="query" name="ws.op"
1173+ required="true"
1174+ fixed="invalidate_token">
1175+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1176+ </wadl:param>
1177+ <wadl:param style="query" required="true"
1178+ name="token">
1179+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1180+The token you want to invalidate
1181+</wadl:doc>
1182+
1183+ </wadl:param>
1184+ <wadl:param style="query" required="true"
1185+ name="consumer_key">
1186+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1187+The consumer key (openid identifier)
1188+</wadl:doc>
1189+
1190+ </wadl:param>
1191+ </wadl:representation>
1192+ </wadl:request>
1193+
1194+ </wadl:method>
1195+ </wadl:resource_type>
1196+
1197+
1198+
1199+ <wadl:resource_type id="captchas">
1200+
1201+ <wadl:method name="GET" id="captchas-get">
1202+ <wadl:response>
1203+ <wadl:representation
1204+ href="{{ baseurl }}/api/1.0/#captcha-page"/>
1205+ <wadl:representation
1206+ mediaType="application/vnd.sun.wadl+xml"
1207+ id="captchas-wadl"/>
1208+ </wadl:response>
1209+ </wadl:method>
1210+
1211+ <wadl:method id="captchas-new" name="POST">
1212+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1213+Generate a new captcha
1214+</wadl:doc>
1215+ <wadl:request>
1216+ <wadl:representation
1217+ mediaType="application/x-www-form-urlencoded">
1218+ <wadl:param style="query" name="ws.op"
1219+ required="true" fixed="new">
1220+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1221+ </wadl:param>
1222+ </wadl:representation>
1223+ </wadl:request>
1224+
1225+ </wadl:method>
1226+ </wadl:resource_type>
1227+
1228+
1229+
1230+ <wadl:resource_type id="registrations">
1231+
1232+ <wadl:method name="GET" id="registrations-get">
1233+ <wadl:response>
1234+ <wadl:representation
1235+ href="{{ baseurl }}/api/1.0/#registration-page"/>
1236+ <wadl:representation
1237+ mediaType="application/vnd.sun.wadl+xml"
1238+ id="registrations-wadl"/>
1239+ </wadl:response>
1240+ </wadl:method>
1241+
1242+ <wadl:method id="registrations-me" name="GET">
1243+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1244+Get details for the currently authenticated user.
1245+</wadl:doc>
1246+ <wadl:request>
1247+
1248+ <wadl:param style="query" name="ws.op"
1249+ required="true" fixed="me">
1250+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1251+ </wadl:param>
1252+
1253+ </wadl:request>
1254+
1255+ </wadl:method>
1256+ <wadl:method id="registrations-team_memberships"
1257+ name="GET">
1258+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1259+Query account for team memberships
1260+</wadl:doc>
1261+ <wadl:request>
1262+
1263+ <wadl:param style="query" name="ws.op"
1264+ required="true"
1265+ fixed="team_memberships">
1266+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1267+ </wadl:param>
1268+ <wadl:param style="query" required="true"
1269+ name="team_names">
1270+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1271+List of team names to check
1272+</wadl:doc>
1273+
1274+ </wadl:param>
1275+
1276+ </wadl:request>
1277+
1278+ </wadl:method>
1279+ <wadl:method id="registrations-validate_email"
1280+ name="GET">
1281+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1282+Validate email by sending token user received in email
1283+</wadl:doc>
1284+ <wadl:request>
1285+
1286+ <wadl:param style="query" name="ws.op"
1287+ required="true"
1288+ fixed="validate_email">
1289+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1290+ </wadl:param>
1291+ <wadl:param style="query" required="true"
1292+ name="email_token">
1293+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1294+Email validation token.
1295+</wadl:doc>
1296+
1297+ </wadl:param>
1298+
1299+ </wadl:request>
1300+
1301+ </wadl:method>
1302+ <wadl:method id="registrations-request_password_reset_token"
1303+ name="POST">
1304+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1305+Request password reset code to be sent to the email
1306+</wadl:doc>
1307+ <wadl:request>
1308+ <wadl:representation
1309+ mediaType="application/x-www-form-urlencoded">
1310+ <wadl:param style="query" name="ws.op"
1311+ required="true"
1312+ fixed="request_password_reset_token">
1313+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1314+ </wadl:param>
1315+ <wadl:param style="query" required="true"
1316+ name="email">
1317+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1318+Email address.
1319+</wadl:doc>
1320+
1321+ </wadl:param>
1322+ </wadl:representation>
1323+ </wadl:request>
1324+
1325+ </wadl:method>
1326+ <wadl:method id="registrations-set_new_password"
1327+ name="POST">
1328+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1329+Set new password for given user
1330+</wadl:doc>
1331+ <wadl:request>
1332+ <wadl:representation
1333+ mediaType="application/x-www-form-urlencoded">
1334+ <wadl:param style="query" name="ws.op"
1335+ required="true"
1336+ fixed="set_new_password">
1337+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1338+ </wadl:param>
1339+ <wadl:param style="query" required="true"
1340+ name="new_password">
1341+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1342+New password
1343+</wadl:doc>
1344+
1345+ </wadl:param>
1346+ <wadl:param style="query" required="true"
1347+ name="token">
1348+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1349+Password reset token.
1350+</wadl:doc>
1351+
1352+ </wadl:param>
1353+ <wadl:param style="query" required="true"
1354+ name="email">
1355+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1356+Email address.
1357+</wadl:doc>
1358+
1359+ </wadl:param>
1360+ </wadl:representation>
1361+ </wadl:request>
1362+
1363+ </wadl:method>
1364+ <wadl:method id="registrations-register" name="POST">
1365+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1366+Generate a new captcha
1367+</wadl:doc>
1368+ <wadl:request>
1369+ <wadl:representation
1370+ mediaType="application/x-www-form-urlencoded">
1371+ <wadl:param style="query" name="ws.op"
1372+ required="true" fixed="register">
1373+ <wadl:doc>The name of the operation being invoked.</wadl:doc>
1374+ </wadl:param>
1375+ <wadl:param style="query" required="true"
1376+ name="captcha_solution">
1377+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1378+Solution for the generated captcha.
1379+</wadl:doc>
1380+
1381+ </wadl:param>
1382+ <wadl:param style="query" required="true"
1383+ name="captcha_id">
1384+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1385+ID for the generated captcha
1386+</wadl:doc>
1387+
1388+ </wadl:param>
1389+ <wadl:param style="query" required="true"
1390+ name="password">
1391+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1392+Password should be at least 8 characters long and contain at least one uppercase letter and a number.
1393+</wadl:doc>
1394+
1395+ </wadl:param>
1396+ <wadl:param style="query" required="false"
1397+ name="displayname">
1398+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1399+Full name
1400+</wadl:doc>
1401+
1402+ </wadl:param>
1403+ <wadl:param style="query" required="true"
1404+ name="email">
1405+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1406+Email address.
1407+</wadl:doc>
1408+
1409+ </wadl:param>
1410+ </wadl:representation>
1411+ </wadl:request>
1412+
1413+ </wadl:method>
1414+ </wadl:resource_type>
1415+
1416+
1417+ <!--End resource_type definitions for collection resources.-->
1418+
1419+ <!--Begin representation and resource_type definitions for entry
1420+ resources and the collections that contain them. -->
1421+
1422+ <wadl:resource_type id="account">
1423+
1424+ <wadl:method name="GET" id="account-get">
1425+ <wadl:response>
1426+ <wadl:representation
1427+ href="{{ baseurl }}/api/1.0/#account-full"/>
1428+ <wadl:representation
1429+ mediaType="application/xhtml+xml" id="account-xhtml"/>
1430+ <wadl:representation
1431+ mediaType="application/vnd.sun.wadl+xml"
1432+ id="account-wadl"/>
1433+ </wadl:response>
1434+ </wadl:method>
1435+
1436+ <wadl:method name="PUT" id="account-put">
1437+ <wadl:request>
1438+ <wadl:representation
1439+ href="{{ baseurl }}/api/1.0/#account-full"/>
1440+ </wadl:request>
1441+ </wadl:method>
1442+
1443+ <wadl:method name="PATCH" id="account-patch">
1444+ <wadl:request>
1445+ <wadl:representation
1446+ href="{{ baseurl }}/api/1.0/#account-diff"/>
1447+ </wadl:request>
1448+ </wadl:method>
1449+
1450+
1451+
1452+ </wadl:resource_type>
1453+
1454+
1455+ <wadl:representation mediaType="application/json"
1456+ id="account-full">
1457+ <wadl:param style="plain" name="self_link" path="$['self_link']">
1458+ <wadl:doc>The canonical link to this resource.</wadl:doc>
1459+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#account"/>
1460+ </wadl:param>
1461+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1462+ <wadl:doc>
1463+ The link to the WADL description of this resource.
1464+ </wadl:doc>
1465+ <wadl:link/>
1466+ </wadl:param>
1467+ <wadl:param style="plain" name="http_etag" path="$['http_etag']">
1468+ <wadl:doc>
1469+ The value of the HTTP ETag for this resource.
1470+ </wadl:doc>
1471+ </wadl:param>
1472+ <wadl:param style="plain" required="true"
1473+ path="$['preferred_email']"
1474+ name="preferred_email">
1475+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1476+Primary email address
1477+</wadl:doc>
1478+
1479+ </wadl:param>
1480+ <wadl:param style="plain" required="true"
1481+ path="$['unverified_emails']"
1482+ name="unverified_emails">
1483+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1484+List of unverified emails
1485+</wadl:doc>
1486+
1487+ </wadl:param>
1488+ <wadl:param style="plain" required="true"
1489+ path="$['id']" name="id">
1490+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1491+Account ID
1492+</wadl:doc>
1493+
1494+ </wadl:param>
1495+ <wadl:param style="plain" required="true"
1496+ path="$['verified_emails']"
1497+ name="verified_emails">
1498+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1499+List of verified emails
1500+</wadl:doc>
1501+
1502+ </wadl:param>
1503+ </wadl:representation>
1504+
1505+ <wadl:representation mediaType="application/json"
1506+ id="account-diff">
1507+ <wadl:param style="plain" required="false"
1508+ path="$['preferred_email']"
1509+ name="preferred_email">
1510+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1511+Primary email address
1512+</wadl:doc>
1513+
1514+ </wadl:param>
1515+ <wadl:param style="plain" required="false"
1516+ path="$['unverified_emails']"
1517+ name="unverified_emails">
1518+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1519+List of unverified emails
1520+</wadl:doc>
1521+
1522+ </wadl:param>
1523+ <wadl:param style="plain" required="false"
1524+ path="$['id']" name="id">
1525+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1526+Account ID
1527+</wadl:doc>
1528+
1529+ </wadl:param>
1530+ <wadl:param style="plain" required="false"
1531+ path="$['verified_emails']"
1532+ name="verified_emails">
1533+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1534+List of verified emails
1535+</wadl:doc>
1536+
1537+ </wadl:param>
1538+ </wadl:representation>
1539+
1540+ <!--Collection page for this type of entry-->
1541+ <wadl:resource_type id="account-page-resource">
1542+ <wadl:method name="GET" id="account-page-resource-get">
1543+ <wadl:response>
1544+ <wadl:representation href="#account-page"/>
1545+ </wadl:response>
1546+ </wadl:method>
1547+ </wadl:resource_type>
1548+
1549+ <wadl:representation mediaType="application/json"
1550+ id="account-page">
1551+
1552+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1553+ <wadl:link/>
1554+ </wadl:param>
1555+
1556+ <wadl:param style="plain" name="total_size" path="$['total_size']" required="true"/>
1557+
1558+ <wadl:param style="plain" name="start" path="$['start']" required="true"/>
1559+
1560+ <wadl:param style="plain" name="next_collection_link" path="$['next_collection_link']">
1561+ <wadl:link resource_type="#account-page-resource"/>
1562+ </wadl:param>
1563+
1564+ <wadl:param style="plain" name="prev_collection_link" path="$['prev_collection_link']">
1565+ <wadl:link resource_type="#account-page-resource"/>
1566+ </wadl:param>
1567+
1568+ <wadl:param style="plain" name="entries" path="$['entries']" required="true"/>
1569+
1570+ <wadl:param style="plain" name="entry_links" path="$['entries'][*]['self_link']">
1571+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#account"/>
1572+ </wadl:param>
1573+ </wadl:representation>
1574+
1575+
1576+
1577+ <wadl:resource_type id="authentication">
1578+
1579+ <wadl:method name="GET" id="authentication-get">
1580+ <wadl:response>
1581+ <wadl:representation
1582+ href="{{ baseurl }}/api/1.0/#authentication-full"/>
1583+ <wadl:representation
1584+ mediaType="application/xhtml+xml"
1585+ id="authentication-xhtml"/>
1586+ <wadl:representation
1587+ mediaType="application/vnd.sun.wadl+xml"
1588+ id="authentication-wadl"/>
1589+ </wadl:response>
1590+ </wadl:method>
1591+
1592+ <wadl:method name="PUT" id="authentication-put">
1593+ <wadl:request>
1594+ <wadl:representation
1595+ href="{{ baseurl }}/api/1.0/#authentication-full"/>
1596+ </wadl:request>
1597+ </wadl:method>
1598+
1599+ <wadl:method name="PATCH" id="authentication-patch">
1600+ <wadl:request>
1601+ <wadl:representation
1602+ href="{{ baseurl }}/api/1.0/#authentication-diff"/>
1603+ </wadl:request>
1604+ </wadl:method>
1605+
1606+
1607+
1608+ </wadl:resource_type>
1609+
1610+
1611+ <wadl:representation mediaType="application/json"
1612+ id="authentication-full">
1613+ <wadl:param style="plain" name="self_link" path="$['self_link']">
1614+ <wadl:doc>The canonical link to this resource.</wadl:doc>
1615+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#authentication"/>
1616+ </wadl:param>
1617+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1618+ <wadl:doc>
1619+ The link to the WADL description of this resource.
1620+ </wadl:doc>
1621+ <wadl:link/>
1622+ </wadl:param>
1623+ <wadl:param style="plain" name="http_etag" path="$['http_etag']">
1624+ <wadl:doc>
1625+ The value of the HTTP ETag for this resource.
1626+ </wadl:doc>
1627+ </wadl:param>
1628+ <wadl:param style="plain" required="true"
1629+ path="$['preferred_email']"
1630+ name="preferred_email">
1631+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1632+Primary email address
1633+</wadl:doc>
1634+
1635+ </wadl:param>
1636+ <wadl:param style="plain" required="true"
1637+ path="$['unverified_emails']"
1638+ name="unverified_emails">
1639+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1640+List of unverified emails
1641+</wadl:doc>
1642+
1643+ </wadl:param>
1644+ <wadl:param style="plain" required="true"
1645+ path="$['id']" name="id">
1646+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1647+Account ID
1648+</wadl:doc>
1649+
1650+ </wadl:param>
1651+ <wadl:param style="plain" required="true"
1652+ path="$['verified_emails']"
1653+ name="verified_emails">
1654+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1655+List of verified emails
1656+</wadl:doc>
1657+
1658+ </wadl:param>
1659+ </wadl:representation>
1660+
1661+ <wadl:representation mediaType="application/json"
1662+ id="authentication-diff">
1663+ <wadl:param style="plain" required="false"
1664+ path="$['preferred_email']"
1665+ name="preferred_email">
1666+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1667+Primary email address
1668+</wadl:doc>
1669+
1670+ </wadl:param>
1671+ <wadl:param style="plain" required="false"
1672+ path="$['unverified_emails']"
1673+ name="unverified_emails">
1674+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1675+List of unverified emails
1676+</wadl:doc>
1677+
1678+ </wadl:param>
1679+ <wadl:param style="plain" required="false"
1680+ path="$['id']" name="id">
1681+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1682+Account ID
1683+</wadl:doc>
1684+
1685+ </wadl:param>
1686+ <wadl:param style="plain" required="false"
1687+ path="$['verified_emails']"
1688+ name="verified_emails">
1689+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1690+List of verified emails
1691+</wadl:doc>
1692+
1693+ </wadl:param>
1694+ </wadl:representation>
1695+
1696+ <!--Collection page for this type of entry-->
1697+ <wadl:resource_type id="authentication-page-resource">
1698+ <wadl:method name="GET"
1699+ id="authentication-page-resource-get">
1700+ <wadl:response>
1701+ <wadl:representation href="#authentication-page"/>
1702+ </wadl:response>
1703+ </wadl:method>
1704+ </wadl:resource_type>
1705+
1706+ <wadl:representation mediaType="application/json"
1707+ id="authentication-page">
1708+
1709+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1710+ <wadl:link/>
1711+ </wadl:param>
1712+
1713+ <wadl:param style="plain" name="total_size" path="$['total_size']" required="true"/>
1714+
1715+ <wadl:param style="plain" name="start" path="$['start']" required="true"/>
1716+
1717+ <wadl:param style="plain" name="next_collection_link" path="$['next_collection_link']">
1718+ <wadl:link resource_type="#authentication-page-resource"/>
1719+ </wadl:param>
1720+
1721+ <wadl:param style="plain" name="prev_collection_link" path="$['prev_collection_link']">
1722+ <wadl:link resource_type="#authentication-page-resource"/>
1723+ </wadl:param>
1724+
1725+ <wadl:param style="plain" name="entries" path="$['entries']" required="true"/>
1726+
1727+ <wadl:param style="plain" name="entry_links" path="$['entries'][*]['self_link']">
1728+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#authentication"/>
1729+ </wadl:param>
1730+ </wadl:representation>
1731+
1732+
1733+
1734+ <wadl:resource_type id="captcha">
1735+
1736+ <wadl:method name="GET" id="captcha-get">
1737+ <wadl:response>
1738+ <wadl:representation
1739+ href="{{ baseurl }}/api/1.0/#captcha-full"/>
1740+ <wadl:representation
1741+ mediaType="application/xhtml+xml" id="captcha-xhtml"/>
1742+ <wadl:representation
1743+ mediaType="application/vnd.sun.wadl+xml"
1744+ id="captcha-wadl"/>
1745+ </wadl:response>
1746+ </wadl:method>
1747+
1748+ <wadl:method name="PUT" id="captcha-put">
1749+ <wadl:request>
1750+ <wadl:representation
1751+ href="{{ baseurl }}/api/1.0/#captcha-full"/>
1752+ </wadl:request>
1753+ </wadl:method>
1754+
1755+ <wadl:method name="PATCH" id="captcha-patch">
1756+ <wadl:request>
1757+ <wadl:representation
1758+ href="{{ baseurl }}/api/1.0/#captcha-diff"/>
1759+ </wadl:request>
1760+ </wadl:method>
1761+
1762+
1763+
1764+ </wadl:resource_type>
1765+
1766+
1767+ <wadl:representation mediaType="application/json"
1768+ id="captcha-full">
1769+ <wadl:param style="plain" name="self_link" path="$['self_link']">
1770+ <wadl:doc>The canonical link to this resource.</wadl:doc>
1771+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#captcha"/>
1772+ </wadl:param>
1773+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1774+ <wadl:doc>
1775+ The link to the WADL description of this resource.
1776+ </wadl:doc>
1777+ <wadl:link/>
1778+ </wadl:param>
1779+ <wadl:param style="plain" name="http_etag" path="$['http_etag']">
1780+ <wadl:doc>
1781+ The value of the HTTP ETag for this resource.
1782+ </wadl:doc>
1783+ </wadl:param>
1784+ <wadl:param style="plain" required="true"
1785+ path="$['content']" name="content">
1786+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1787+Captcha content
1788+</wadl:doc>
1789+
1790+ </wadl:param>
1791+ <wadl:param style="plain" required="true"
1792+ path="$['id']" name="id">
1793+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1794+Captcha ID
1795+</wadl:doc>
1796+
1797+ </wadl:param>
1798+ </wadl:representation>
1799+
1800+ <wadl:representation mediaType="application/json"
1801+ id="captcha-diff">
1802+ <wadl:param style="plain" required="false"
1803+ path="$['content']" name="content">
1804+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1805+Captcha content
1806+</wadl:doc>
1807+
1808+ </wadl:param>
1809+ <wadl:param style="plain" required="false"
1810+ path="$['id']" name="id">
1811+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1812+Captcha ID
1813+</wadl:doc>
1814+
1815+ </wadl:param>
1816+ </wadl:representation>
1817+
1818+ <!--Collection page for this type of entry-->
1819+ <wadl:resource_type id="captcha-page-resource">
1820+ <wadl:method name="GET" id="captcha-page-resource-get">
1821+ <wadl:response>
1822+ <wadl:representation href="#captcha-page"/>
1823+ </wadl:response>
1824+ </wadl:method>
1825+ </wadl:resource_type>
1826+
1827+ <wadl:representation mediaType="application/json"
1828+ id="captcha-page">
1829+
1830+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1831+ <wadl:link/>
1832+ </wadl:param>
1833+
1834+ <wadl:param style="plain" name="total_size" path="$['total_size']" required="true"/>
1835+
1836+ <wadl:param style="plain" name="start" path="$['start']" required="true"/>
1837+
1838+ <wadl:param style="plain" name="next_collection_link" path="$['next_collection_link']">
1839+ <wadl:link resource_type="#captcha-page-resource"/>
1840+ </wadl:param>
1841+
1842+ <wadl:param style="plain" name="prev_collection_link" path="$['prev_collection_link']">
1843+ <wadl:link resource_type="#captcha-page-resource"/>
1844+ </wadl:param>
1845+
1846+ <wadl:param style="plain" name="entries" path="$['entries']" required="true"/>
1847+
1848+ <wadl:param style="plain" name="entry_links" path="$['entries'][*]['self_link']">
1849+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#captcha"/>
1850+ </wadl:param>
1851+ </wadl:representation>
1852+
1853+
1854+
1855+ <wadl:resource_type id="registration">
1856+
1857+ <wadl:method name="GET" id="registration-get">
1858+ <wadl:response>
1859+ <wadl:representation
1860+ href="{{ baseurl }}/api/1.0/#registration-full"/>
1861+ <wadl:representation
1862+ mediaType="application/xhtml+xml"
1863+ id="registration-xhtml"/>
1864+ <wadl:representation
1865+ mediaType="application/vnd.sun.wadl+xml"
1866+ id="registration-wadl"/>
1867+ </wadl:response>
1868+ </wadl:method>
1869+
1870+ <wadl:method name="PUT" id="registration-put">
1871+ <wadl:request>
1872+ <wadl:representation
1873+ href="{{ baseurl }}/api/1.0/#registration-full"/>
1874+ </wadl:request>
1875+ </wadl:method>
1876+
1877+ <wadl:method name="PATCH" id="registration-patch">
1878+ <wadl:request>
1879+ <wadl:representation
1880+ href="{{ baseurl }}/api/1.0/#registration-diff"/>
1881+ </wadl:request>
1882+ </wadl:method>
1883+
1884+
1885+
1886+ </wadl:resource_type>
1887+
1888+
1889+ <wadl:representation mediaType="application/json"
1890+ id="registration-full">
1891+ <wadl:param style="plain" name="self_link" path="$['self_link']">
1892+ <wadl:doc>The canonical link to this resource.</wadl:doc>
1893+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#registration"/>
1894+ </wadl:param>
1895+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1896+ <wadl:doc>
1897+ The link to the WADL description of this resource.
1898+ </wadl:doc>
1899+ <wadl:link/>
1900+ </wadl:param>
1901+ <wadl:param style="plain" name="http_etag" path="$['http_etag']">
1902+ <wadl:doc>
1903+ The value of the HTTP ETag for this resource.
1904+ </wadl:doc>
1905+ </wadl:param>
1906+ <wadl:param style="plain" required="true"
1907+ path="$['preferred_email']"
1908+ name="preferred_email">
1909+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1910+Primary email address
1911+</wadl:doc>
1912+
1913+ </wadl:param>
1914+ <wadl:param style="plain" required="true"
1915+ path="$['unverified_emails']"
1916+ name="unverified_emails">
1917+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1918+List of unverified emails
1919+</wadl:doc>
1920+
1921+ </wadl:param>
1922+ <wadl:param style="plain" required="true"
1923+ path="$['id']" name="id">
1924+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1925+Account ID
1926+</wadl:doc>
1927+
1928+ </wadl:param>
1929+ <wadl:param style="plain" required="true"
1930+ path="$['verified_emails']"
1931+ name="verified_emails">
1932+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1933+List of verified emails
1934+</wadl:doc>
1935+
1936+ </wadl:param>
1937+ </wadl:representation>
1938+
1939+ <wadl:representation mediaType="application/json"
1940+ id="registration-diff">
1941+ <wadl:param style="plain" required="false"
1942+ path="$['preferred_email']"
1943+ name="preferred_email">
1944+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1945+Primary email address
1946+</wadl:doc>
1947+
1948+ </wadl:param>
1949+ <wadl:param style="plain" required="false"
1950+ path="$['unverified_emails']"
1951+ name="unverified_emails">
1952+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1953+List of unverified emails
1954+</wadl:doc>
1955+
1956+ </wadl:param>
1957+ <wadl:param style="plain" required="false"
1958+ path="$['id']" name="id">
1959+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1960+Account ID
1961+</wadl:doc>
1962+
1963+ </wadl:param>
1964+ <wadl:param style="plain" required="false"
1965+ path="$['verified_emails']"
1966+ name="verified_emails">
1967+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
1968+List of verified emails
1969+</wadl:doc>
1970+
1971+ </wadl:param>
1972+ </wadl:representation>
1973+
1974+ <!--Collection page for this type of entry-->
1975+ <wadl:resource_type id="registration-page-resource">
1976+ <wadl:method name="GET"
1977+ id="registration-page-resource-get">
1978+ <wadl:response>
1979+ <wadl:representation href="#registration-page"/>
1980+ </wadl:response>
1981+ </wadl:method>
1982+ </wadl:resource_type>
1983+
1984+ <wadl:representation mediaType="application/json"
1985+ id="registration-page">
1986+
1987+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
1988+ <wadl:link/>
1989+ </wadl:param>
1990+
1991+ <wadl:param style="plain" name="total_size" path="$['total_size']" required="true"/>
1992+
1993+ <wadl:param style="plain" name="start" path="$['start']" required="true"/>
1994+
1995+ <wadl:param style="plain" name="next_collection_link" path="$['next_collection_link']">
1996+ <wadl:link resource_type="#registration-page-resource"/>
1997+ </wadl:param>
1998+
1999+ <wadl:param style="plain" name="prev_collection_link" path="$['prev_collection_link']">
2000+ <wadl:link resource_type="#registration-page-resource"/>
2001+ </wadl:param>
2002+
2003+ <wadl:param style="plain" name="entries" path="$['entries']" required="true"/>
2004+
2005+ <wadl:param style="plain" name="entry_links" path="$['entries'][*]['self_link']">
2006+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#registration"/>
2007+ </wadl:param>
2008+ </wadl:representation>
2009+
2010+
2011+
2012+ <wadl:resource_type id="validation">
2013+
2014+ <wadl:method name="GET" id="validation-get">
2015+ <wadl:response>
2016+ <wadl:representation
2017+ href="{{ baseurl }}/api/1.0/#validation-full"/>
2018+ <wadl:representation
2019+ mediaType="application/xhtml+xml" id="validation-xhtml"/>
2020+ <wadl:representation
2021+ mediaType="application/vnd.sun.wadl+xml"
2022+ id="validation-wadl"/>
2023+ </wadl:response>
2024+ </wadl:method>
2025+
2026+ <wadl:method name="PUT" id="validation-put">
2027+ <wadl:request>
2028+ <wadl:representation
2029+ href="{{ baseurl }}/api/1.0/#validation-full"/>
2030+ </wadl:request>
2031+ </wadl:method>
2032+
2033+ <wadl:method name="PATCH" id="validation-patch">
2034+ <wadl:request>
2035+ <wadl:representation
2036+ href="{{ baseurl }}/api/1.0/#validation-diff"/>
2037+ </wadl:request>
2038+ </wadl:method>
2039+
2040+
2041+
2042+ </wadl:resource_type>
2043+
2044+
2045+ <wadl:representation mediaType="application/json"
2046+ id="validation-full">
2047+ <wadl:param style="plain" name="self_link" path="$['self_link']">
2048+ <wadl:doc>The canonical link to this resource.</wadl:doc>
2049+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#validation"/>
2050+ </wadl:param>
2051+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
2052+ <wadl:doc>
2053+ The link to the WADL description of this resource.
2054+ </wadl:doc>
2055+ <wadl:link/>
2056+ </wadl:param>
2057+ <wadl:param style="plain" name="http_etag" path="$['http_etag']">
2058+ <wadl:doc>
2059+ The value of the HTTP ETag for this resource.
2060+ </wadl:doc>
2061+ </wadl:param>
2062+ <wadl:param style="plain" required="true"
2063+ path="$['preferred_email']"
2064+ name="preferred_email">
2065+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2066+Primary email address
2067+</wadl:doc>
2068+
2069+ </wadl:param>
2070+ <wadl:param style="plain" required="true"
2071+ path="$['unverified_emails']"
2072+ name="unverified_emails">
2073+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2074+List of unverified emails
2075+</wadl:doc>
2076+
2077+ </wadl:param>
2078+ <wadl:param style="plain" required="true"
2079+ path="$['id']" name="id">
2080+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2081+Account ID
2082+</wadl:doc>
2083+
2084+ </wadl:param>
2085+ <wadl:param style="plain" required="true"
2086+ path="$['verified_emails']"
2087+ name="verified_emails">
2088+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2089+List of verified emails
2090+</wadl:doc>
2091+
2092+ </wadl:param>
2093+ </wadl:representation>
2094+
2095+ <wadl:representation mediaType="application/json"
2096+ id="validation-diff">
2097+ <wadl:param style="plain" required="false"
2098+ path="$['preferred_email']"
2099+ name="preferred_email">
2100+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2101+Primary email address
2102+</wadl:doc>
2103+
2104+ </wadl:param>
2105+ <wadl:param style="plain" required="false"
2106+ path="$['unverified_emails']"
2107+ name="unverified_emails">
2108+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2109+List of unverified emails
2110+</wadl:doc>
2111+
2112+ </wadl:param>
2113+ <wadl:param style="plain" required="false"
2114+ path="$['id']" name="id">
2115+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2116+Account ID
2117+</wadl:doc>
2118+
2119+ </wadl:param>
2120+ <wadl:param style="plain" required="false"
2121+ path="$['verified_emails']"
2122+ name="verified_emails">
2123+ <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
2124+List of verified emails
2125+</wadl:doc>
2126+
2127+ </wadl:param>
2128+ </wadl:representation>
2129+
2130+ <!--Collection page for this type of entry-->
2131+ <wadl:resource_type id="validation-page-resource">
2132+ <wadl:method name="GET"
2133+ id="validation-page-resource-get">
2134+ <wadl:response>
2135+ <wadl:representation href="#validation-page"/>
2136+ </wadl:response>
2137+ </wadl:method>
2138+ </wadl:resource_type>
2139+
2140+ <wadl:representation mediaType="application/json"
2141+ id="validation-page">
2142+
2143+ <wadl:param style="plain" name="resource_type_link" path="$['resource_type_link']">
2144+ <wadl:link/>
2145+ </wadl:param>
2146+
2147+ <wadl:param style="plain" name="total_size" path="$['total_size']" required="true"/>
2148+
2149+ <wadl:param style="plain" name="start" path="$['start']" required="true"/>
2150+
2151+ <wadl:param style="plain" name="next_collection_link" path="$['next_collection_link']">
2152+ <wadl:link resource_type="#validation-page-resource"/>
2153+ </wadl:param>
2154+
2155+ <wadl:param style="plain" name="prev_collection_link" path="$['prev_collection_link']">
2156+ <wadl:link resource_type="#validation-page-resource"/>
2157+ </wadl:param>
2158+
2159+ <wadl:param style="plain" name="entries" path="$['entries']" required="true"/>
2160+
2161+ <wadl:param style="plain" name="entry_links" path="$['entries'][*]['self_link']">
2162+ <wadl:link resource_type="{{ baseurl }}/api/1.0/#validation"/>
2163+ </wadl:param>
2164+ </wadl:representation>
2165+
2166+
2167+ <!--End representation and resource_type definitions for entry
2168+ resources. -->
2169+
2170+ <!--Finally, describe the 'hosted binary file' type.-->
2171+ <wadl:resource_type id="HostedFile">
2172+ <wadl:method name="GET" id="HostedFile-get">
2173+ <wadl:response>
2174+ <wadl:representation status="303">
2175+ <wadl:param style="header" name="Location"/>
2176+ </wadl:representation>
2177+ </wadl:response>
2178+ </wadl:method>
2179+ <wadl:method name="PUT" id="HostedFile-put"/>
2180+ <wadl:method name="DELETE" id="HostedFile-delete"/>
2181+ </wadl:resource_type>
2182+
2183+ <!--Define a data type for binary data.-->
2184+ <xsd:simpleType name="binary">
2185+ <xsd:list itemType="byte"/>
2186+ </xsd:simpleType>
2187+
2188+</wadl:application>
2189
2190=== modified file 'identityprovider/tests/test_forms.py'
2191--- identityprovider/tests/test_forms.py 2011-01-19 20:57:48 +0000
2192+++ identityprovider/tests/test_forms.py 2011-05-24 16:31:52 +0000
2193@@ -6,8 +6,8 @@
2194 from django.conf import settings
2195 from identityprovider.models.account import Account
2196 from identityprovider.forms import EditAccountForm, ResetPasswordForm
2197-from identityprovider.webservice import forms
2198-from identityprovider.webservice.forms import WebserviceCreateAccountForm
2199+from identityprovider.api10 import forms
2200+from identityprovider.api10.forms import WebserviceCreateAccountForm
2201 from utils import BasicAccountTestCase, SQLCachedTestCase
2202
2203
2204@@ -71,17 +71,11 @@
2205 class WebServiceCreateAccountFormTest(SQLCachedTestCase):
2206
2207 def setUp(self):
2208+ self.old_dcv = settings.DISABLE_CAPTCHA_VERIFICATION
2209 settings.DISABLE_CAPTCHA_VERIFICATION = True
2210
2211- class Request(object):
2212- def __init__(self):
2213- self.environment = {}
2214- self.form = {'captcha_id': None, 'captcha_solution': None}
2215- self.old_get_request = forms.get_current_browser_request
2216- forms.get_current_browser_request = lambda: Request()
2217-
2218 def tearDown(self):
2219- forms.get_current_browser_request = self.old_get_request
2220+ settings.DISABLE_CAPTCHA_VERIFICATION = self.old_dcv
2221
2222 def test_nonascii_password(self):
2223 data = {'password': 'Curuzú Cuatiá',
2224
2225=== renamed file 'identityprovider/tests/test_webservice.py' => 'identityprovider/tests/test_handlers.py'
2226--- identityprovider/tests/test_webservice.py 2011-03-14 12:51:02 +0000
2227+++ identityprovider/tests/test_handlers.py 2011-05-24 16:31:52 +0000
2228@@ -1,72 +1,112 @@
2229 from mock import patch
2230
2231 from identityprovider.tests.utils import SQLCachedTestCase
2232-from identityprovider.models.authtoken import (AuthTokenFactory,
2233- LoginTokenType,
2234- AuthToken)
2235-from identityprovider.models.api import APIUser
2236-from identityprovider.models.account import Account
2237-from identityprovider.models.const import AccountStatus
2238-from identityprovider.webservice.models import (
2239- RegistrationSet, CanNotResetPasswordError, AuthenticationSet)
2240-
2241-
2242-class RegistrationsTestCase(SQLCachedTestCase):
2243+from identityprovider.models import (
2244+ Account,
2245+ APIUser,
2246+ AuthToken,
2247+ AuthTokenFactory,
2248+ EmailAddress,
2249+)
2250+from identityprovider.models.const import (
2251+ AccountStatus,
2252+ EmailStatus,
2253+ LoginTokenType,
2254+)
2255+from identityprovider.api10.handlers import (
2256+ AuthenticationHandler,
2257+ CanNotResetPasswordError,
2258+ RegistrationHandler,
2259+)
2260+
2261+class MockRequest(object):
2262+ def __init__(self, data=None, user=None):
2263+ self.user = user
2264+ if data is None:
2265+ data = {}
2266+ self.data = data
2267+ self.environ = {'REMOTE_ADDR': '127.0.0.1'}
2268+
2269+class RegistrationHandlerTestCase(SQLCachedTestCase):
2270
2271 fixtures = ["test"]
2272 pgsql_functions = ["generate_openid_identifier"]
2273
2274 def setUp(self):
2275- self.registration = RegistrationSet()
2276+ self.registration = RegistrationHandler()
2277
2278 def test_authtoken_without_requester_is_handled_properly(self):
2279 authtoken = AuthTokenFactory().new(None, None, "test@example.com",
2280 LoginTokenType.PASSWORDRECOVERY,
2281 None)
2282+ request = MockRequest(data={'email': "test@example.com",
2283+ 'token': authtoken.token, 'new_password': "pass"})
2284
2285- response = self.registration.set_new_password("test@example.com",
2286- authtoken.token, "pass")
2287+ response = self.registration.set_new_password(request)
2288
2289 self.assertEquals(response['status'], "error")
2290
2291 def test_reset_token_when_email_is_invalid(self):
2292+ request = MockRequest(data={'email': "non-existing@example.com"})
2293+
2294 self.assertRaises(CanNotResetPasswordError,
2295 self.registration.request_password_reset_token,
2296- "non-existing@example.com")
2297+ request)
2298
2299 def test_reset_token_when_account_is_disabled(self):
2300 account = Account.objects.get_by_email("mark@example.com")
2301 account.status = AccountStatus.SUSPENDED
2302 account.save()
2303+ request = MockRequest(data={'email': "mark@example.com"})
2304
2305 self.assertRaises(CanNotResetPasswordError,
2306 self.registration.request_password_reset_token,
2307- "mark@example.com")
2308+ request)
2309
2310 def test_reset_password_when_not_existing_token_is_passed(self):
2311+ request = MockRequest(data={'email': "mark@example.com",
2312+ 'token': "not-existing-token", 'new_password': "password"})
2313+
2314 self.assertRaises(AuthToken.DoesNotExist,
2315- self.registration.set_new_password,
2316- "mark@example.com", "not-existing-token",
2317- "password")
2318+ self.registration.set_new_password, request)
2319+
2320+ def test_register_email_error_is_in_list(self):
2321+ request = MockRequest(data={
2322+ 'email': "register@example.com",
2323+ 'password': "blogdf3Daa",
2324+ 'captcha_solution': "solution",
2325+ 'captcha_id': "id",
2326+ })
2327+ EmailAddress.objects.create(email="register@example.com",
2328+ status=EmailStatus.NEW)
2329+
2330+ registration = RegistrationHandler()
2331+ r = registration.register(request)
2332+
2333+ self.assertEquals(r['errors'], {'email': ["Email already registered"]})
2334
2335 def test_register_without_displayname(self):
2336- response = self.registration.register(
2337- email='test@example.com',
2338- password='MySecretPassword1',
2339- captcha_id=1,
2340- captcha_solution='foobar')
2341+ request = MockRequest(data={
2342+ 'email': "test@example.com",
2343+ 'password': "MySecretPassword1",
2344+ 'captcha_solution': "foobar",
2345+ 'captcha_id': "id",
2346+ })
2347+ response = self.registration.register(request)
2348 self.assertEqual(response['status'], 'ok')
2349
2350 account = Account.objects.get_by_email('test@example.com')
2351 self.assertEqual(account.displayname, '')
2352
2353 def test_register_with_displayname(self):
2354- response = self.registration.register(
2355- email='test@example.com',
2356- password='MySecretPassword1',
2357- captcha_id=1,
2358- captcha_solution='foobar',
2359- displayname='Test User')
2360+ request = MockRequest(data={
2361+ 'email': "test@example.com",
2362+ 'password': "MySecretPassword1",
2363+ 'captcha_solution': "foobar",
2364+ 'captcha_id': "id",
2365+ 'displayname': 'Test User',
2366+ })
2367+ response = self.registration.register(request)
2368 self.assertEqual(response['status'], 'ok')
2369
2370 account = Account.objects.get_by_email('test@example.com')
2371@@ -78,24 +118,18 @@
2372 fixtures = ["test"]
2373
2374 def setUp(self):
2375- self.authentication = AuthenticationSet()
2376-
2377- @patch('identityprovider.webservice.models.get_current_browser_request')
2378- def test_account_by_openid_with_valid_openid(self, mock_get_request):
2379- mock_get_request.return_value.environment = {
2380- 'authenticated_user': APIUser()
2381- }
2382-
2383- account = self.authentication.account_by_openid(openid="mark_oid")
2384+ self.authentication = AuthenticationHandler()
2385+
2386+ def test_account_by_openid_with_valid_openid(self):
2387+ request = MockRequest(user=APIUser(), data={'openid': "mark_oid"})
2388+
2389+ account = self.authentication.account_by_openid(request)
2390
2391 self.assertEquals(account['openid_identifier'], "mark_oid")
2392
2393- @patch('identityprovider.webservice.models.get_current_browser_request')
2394- def test_account_by_openid_with_invalid_openid(self, mock_get_request):
2395- mock_get_request.return_value.environment = {
2396- 'authenticated_user': APIUser()
2397- }
2398+ def test_account_by_openid_with_invalid_openid(self):
2399+ request = MockRequest(user=APIUser(), data={'openid': "bad-openid"})
2400
2401- account = self.authentication.account_by_openid(openid="bad-openid")
2402+ account = self.authentication.account_by_openid(request)
2403
2404 self.assertTrue(account is None)
2405
2406=== modified file 'identityprovider/tests/test_models_account.py'
2407--- identityprovider/tests/test_models_account.py 2011-01-11 15:50:14 +0000
2408+++ identityprovider/tests/test_models_account.py 2011-05-24 16:31:52 +0000
2409@@ -15,7 +15,6 @@
2410 from identityprovider.models.const import AccountCreationRationale, EmailStatus
2411 from identityprovider.readonly import ReadOnlyManager
2412 from identityprovider.utils import encrypt_launchpad_password, generate_salt
2413-from identityprovider.webservice.models import AccountSet
2414
2415
2416 class MockBackend(object):
2417@@ -242,16 +241,6 @@
2418 password_count = AccountPassword.objects.filter(account=account).count()
2419 self.assertEqual(password_count, 0)
2420
2421- def test_parent(self):
2422- account = self.get_existing_account()
2423- parent = getattr(account, '__parent__')
2424- self.assertTrue(isinstance(parent, AccountSet))
2425-
2426- def test_url_path(self):
2427- account = self.get_existing_account()
2428- url_path = getattr(account, '__url_path__')
2429- self.assertEqual(url_path, str(account.id))
2430-
2431 def test_create_account_with_rationale(self):
2432 salt = generate_salt()
2433 account = Account.objects.create_account('displayname', 'username',
2434
2435=== modified file 'identityprovider/tests/test_models_api.py'
2436--- identityprovider/tests/test_models_api.py 2010-09-22 14:36:08 +0000
2437+++ identityprovider/tests/test_models_api.py 2011-05-24 16:31:52 +0000
2438@@ -5,8 +5,6 @@
2439 from identityprovider.models.emailaddress import EmailAddress
2440 from identityprovider.models.const import EmailStatus
2441 from identityprovider.models.api import APIUser
2442-from identityprovider.webservice.models import RegistrationSet
2443-from identityprovider.webservice import forms
2444 from identityprovider.tests.utils import SQLCachedTestCase
2445 from identityprovider.utils import encrypt_launchpad_password, generate_salt
2446
2447@@ -45,22 +43,3 @@
2448 def test_unicode(self):
2449 self.assertEqual(unicode(self.user), u'username')
2450
2451-
2452-class RegistrationSetTestCase(TestCase):
2453-
2454- def test_register_email_error_is_in_list(self):
2455- old_gcbr = forms.get_current_browser_request
2456- forms.get_current_browser_request = lambda: self
2457- self.environment = {}
2458- EmailAddress.objects.create(email="register@example.com",
2459- status=EmailStatus.NEW)
2460-
2461- registration = RegistrationSet()
2462- r = registration.register(email="register@example.com",
2463- password="blogdf3Daa",
2464- captcha_solution="solution",
2465- captcha_id="id")
2466-
2467- self.assertEquals(r['errors'], {'email': ["Email already registered"]})
2468-
2469- forms.get_current_browser_request = old_gcbr
2470
2471=== removed file 'identityprovider/tests/test_wsgi.py'
2472--- identityprovider/tests/test_wsgi.py 2010-11-15 16:24:53 +0000
2473+++ identityprovider/tests/test_wsgi.py 1970-01-01 00:00:00 +0000
2474@@ -1,71 +0,0 @@
2475-from mock import Mock
2476-
2477-from unittest import TestCase
2478-from identityprovider.wsgi import WSGIDispatch, make_app
2479-
2480-
2481-
2482-class StubWSGIApp(object):
2483-
2484- def __init__(self):
2485- self.called = False
2486-
2487- def __call__(self, environ, start_response):
2488- self.called = True
2489- return []
2490-
2491-
2492-
2493-class WSGIDispatchTestCase(TestCase):
2494-
2495-
2496- def setUp(self):
2497- self.app_1 = StubWSGIApp()
2498- self.app_2 = StubWSGIApp()
2499- self.default_app = StubWSGIApp()
2500- self.mapping = (('/a1', self.app_1), ('/a2', self.app_2))
2501- self.dispatch = WSGIDispatch(self.mapping, self.default_app)
2502-
2503-
2504- def call_dispatch_with_path(self, path):
2505- self.dispatch({'PATH_INFO': path}, lambda x: x)
2506-
2507-
2508- def test_call_is_routed_to_the_right_application(self):
2509- self.call_dispatch_with_path('/a1')
2510-
2511- self.assertTrue(self.app_1.called)
2512-
2513-
2514- def test_call_is_routed_to_default_if_path_is_not_matching(self):
2515- self.call_dispatch_with_path('/other')
2516-
2517- self.assertTrue(self.default_app.called)
2518-
2519-
2520-
2521-class MakeAppTestCase(TestCase):
2522-
2523-
2524- def test_make_sure_that_callable_is_returned(self):
2525- app = make_app()
2526-
2527- self.assertTrue(callable(app))
2528-
2529-
2530- def test_oops_from_api_contain_traceback(self):
2531- environ = {
2532- 'REQUEST_METHOD': 'get',
2533- 'SERVER_NAME': 'test',
2534- 'SERVER_PORT': 80,
2535- 'PATH_INFO': '/api/1.0/',
2536- }
2537-
2538- mock_app = Mock()
2539- mock_app.side_effect = Exception("App error")
2540-
2541- app = make_app()
2542- app.mapping[0][1].application = mock_app
2543- app(environ, Mock())
2544-
2545- self.assertTrue('oops-dump' in environ)
2546
2547=== modified file 'identityprovider/urls.py'
2548--- identityprovider/urls.py 2011-03-04 17:09:45 +0000
2549+++ identityprovider/urls.py 2011-05-24 16:31:52 +0000
2550@@ -126,3 +126,8 @@
2551 urlpatterns += patterns('identityprovider.views.ui',
2552 (r'^\+(?P<page_name>description|faq)$', 'static_page'),
2553 )
2554+
2555+# Lazr.restful backwards compatible api
2556+urlpatterns += patterns('',
2557+ (r'api/1.0/', include('identityprovider.api10.urls')),
2558+)
2559
2560=== removed directory 'identityprovider/webservice'
2561=== removed file 'identityprovider/webservice/__init__.py'
2562--- identityprovider/webservice/__init__.py 2010-04-21 15:29:24 +0000
2563+++ identityprovider/webservice/__init__.py 1970-01-01 00:00:00 +0000
2564@@ -1,2 +0,0 @@
2565-# Copyright 2010 Canonical Ltd. This software is licensed under the
2566-# GNU Affero General Public License version 3 (see the file LICENSE).
2567
2568=== removed file 'identityprovider/webservice/forms.py'
2569--- identityprovider/webservice/forms.py 2011-03-14 12:51:02 +0000
2570+++ identityprovider/webservice/forms.py 1970-01-01 00:00:00 +0000
2571@@ -1,65 +0,0 @@
2572-# Copyright 2010 Canonical Ltd. This software is licensed under the
2573-# GNU Affero General Public License version 3 (see the file LICENSE).
2574-
2575-# We use Django forms for webservice input validation
2576-
2577-from django import forms
2578-from django.forms import fields
2579-from django.utils.translation import ugettext as _
2580-from lazr.restful.utils import get_current_browser_request
2581-
2582-from identityprovider.utils import password_policy_compliant
2583-from identityprovider.models.captcha import Captcha, VerifyCaptchaError
2584-
2585-
2586-PASSWORD_POLICY_ERROR = _("Password must be at least "
2587- "8 characters long, and must contain at least one "
2588- "number and an upper case letter.")
2589-
2590-
2591-class WebserviceCreateAccountForm(forms.Form):
2592- email = fields.EmailField()
2593- password = fields.CharField(max_length=256)
2594- captcha_id = fields.CharField(max_length=1024)
2595- captcha_solution = fields.CharField(max_length=256)
2596- displayname = fields.CharField(required=False)
2597- remote_ip = fields.CharField(max_length=256)
2598-
2599- def clean_password(self):
2600- if 'password' in self.cleaned_data:
2601- password = self.cleaned_data['password']
2602- try:
2603- str(password)
2604- except UnicodeEncodeError:
2605- raise forms.ValidationError(
2606- _("Invalid characters in password"))
2607- if not password_policy_compliant(password):
2608- raise forms.ValidationError(PASSWORD_POLICY_ERROR)
2609- return password
2610-
2611- def clean(self):
2612- cleaned_data = self.cleaned_data
2613- captcha_id = cleaned_data.get('captcha_id')
2614- captcha_solution = cleaned_data.get('captcha_solution')
2615-
2616- # The remote IP address is absolutely required, and comes from
2617- # SSO itself, not from the client. If it's missing, it's a
2618- # programming error, and should not be returned to the client
2619- # as a validation error. So, we use a normal key lookup here.
2620- remote_ip = cleaned_data['remote_ip']
2621-
2622- request = get_current_browser_request()
2623-
2624- captcha = Captcha(captcha_id)
2625-
2626- try:
2627- if captcha.verify(captcha_solution, remote_ip):
2628- return cleaned_data
2629- except VerifyCaptchaError, e:
2630- if getattr(request.environment, '__setitem__', False):
2631- request.environment['oops-dump'] = True
2632- logging.warning(e.traceback)
2633- logging.warning("reCaptcha connection error")
2634-
2635- # not verified
2636- raise forms.ValidationError(_("Wrong captcha solution."))
2637
2638=== removed file 'identityprovider/webservice/interfaces.py'
2639--- identityprovider/webservice/interfaces.py 2011-03-14 12:51:02 +0000
2640+++ identityprovider/webservice/interfaces.py 1970-01-01 00:00:00 +0000
2641@@ -1,191 +0,0 @@
2642-# Copyright 2010 Canonical Ltd. This software is licensed under the
2643-# GNU Affero General Public License version 3 (see the file LICENSE).
2644-
2645-from lazr.restful.frameworks.django import IDjangoLocation
2646-from lazr.restful.declarations import (export_as_webservice_entry,
2647- collection_default_content, exported, export_write_operation,
2648- export_as_webservice_collection, operation_parameters,
2649- export_read_operation)
2650-from zope.schema import Text, TextLine, List
2651-
2652-
2653-__all__ = [
2654- 'IAccount',
2655- 'ICaptcha',
2656- 'IAccountSet',
2657- 'ICaptchaSet',
2658- 'IRegistrationSet',
2659- 'IValidationSet',
2660- 'IAuthenticationSet'
2661-]
2662-
2663-
2664-class IAccount(IDjangoLocation):
2665- export_as_webservice_entry()
2666- id = exported(Text(title=u"Account ID"))
2667- preferred_email = exported(Text(title=u"Primary email address"))
2668- verified_emails = exported(List(title=u"List of verified emails",
2669- value_type=Text()))
2670- unverified_emails = exported(List(value_type=Text(),
2671- title=u"List of unverified emails"))
2672-
2673-
2674-class IRegistration(IAccount):
2675- export_as_webservice_entry()
2676-
2677-
2678-class IValidation(IAccount):
2679- export_as_webservice_entry()
2680-
2681-
2682-class IAuthentication(IAccount):
2683- export_as_webservice_entry()
2684-
2685-
2686-class ICaptcha(IDjangoLocation):
2687- export_as_webservice_entry()
2688- id = exported(Text(title=u"Captcha ID"))
2689- content = exported(Text(title=u"Captcha content"))
2690-
2691-
2692-class IAccountSet(IDjangoLocation):
2693- export_as_webservice_collection(IAccount)
2694-
2695- @collection_default_content()
2696- def getAll():
2697- """Fail, as no default content should be provided."""
2698-
2699- def get(request, unique_id):
2700- """Retrieve a account by its ID."""
2701-
2702- @export_read_operation()
2703- def me():
2704- """ Get details for the currently authenticated user. """
2705-
2706- @operation_parameters(
2707- team_names=List(title=u"List of team names to check",
2708- value_type=Text())
2709- )
2710- @export_read_operation()
2711- def team_memberships(team_names):
2712- """Query account for team memberships"""
2713-
2714- @operation_parameters(
2715- email_token=TextLine(title=u"Email validation token."))
2716- @export_read_operation()
2717- def validate_email(email_token):
2718- """Validate email by sending token user received in email"""
2719-
2720-
2721-class IAuthenticationSet(IAccountSet):
2722- export_as_webservice_collection(IAuthentication)
2723-
2724- @collection_default_content()
2725- def getAll():
2726- """Fail, as no default content should be provided."""
2727-
2728- @operation_parameters(
2729- token_name=TextLine(title=u"Token name."))
2730- @export_read_operation()
2731- def authenticate(token_name):
2732- """Obtain OAuth token for logged in user"""
2733-
2734- @operation_parameters(
2735- consumer_key=TextLine(title=u"User's OpenID identifier"))
2736- @export_read_operation()
2737- def list_tokens(consumer_key):
2738- """ Get the currently valid tokens for a given user """
2739-
2740- @operation_parameters(
2741- token=TextLine(title=u"The token you want to validate"),
2742- consumer_key=TextLine(title=u"The consumer key (openid identifier)")
2743- )
2744- @export_read_operation()
2745- def validate_token(token, consumer_key):
2746- """Check that a token is valid.
2747-
2748- If valid, this method return the token and consumer secrets
2749- """
2750-
2751- @operation_parameters(
2752- token=TextLine(title=u"The token you want to invalidate"),
2753- consumer_key=TextLine(title=u"The consumer key (openid identifier)")
2754- )
2755- @export_write_operation()
2756- def invalidate_token(token, consumer_key):
2757- """ Make the given token invalid """
2758-
2759- @operation_parameters(
2760- team_names=List(title=u"List of team names to check",
2761- value_type=Text()),
2762- openid_identifier=Text(title=u"OpenID Identifier used for asking for"
2763- u"team memberships on behalf of other user.")
2764- )
2765- @export_read_operation()
2766- def team_memberships(team_names, openid_identifier):
2767- """Query user for team memberships"""
2768-
2769- @operation_parameters(
2770- email=Text(title=u"Email address of the account"))
2771- @export_read_operation()
2772- def account_by_email(email):
2773- """Retrieve account information based on email address"""
2774-
2775- @operation_parameters(
2776- openid=Text(title=u"OpenID Identifier of the account"))
2777- @export_read_operation()
2778- def account_by_openid(openid):
2779- """Retrieve account information based on OpenID identifier"""
2780-
2781-
2782-class IValidationSet(IAccountSet):
2783- pass
2784-
2785-
2786-class IRegistrationSet(IAccountSet):
2787- export_as_webservice_collection(IRegistration)
2788-
2789- @collection_default_content()
2790- def getAll():
2791- """Fail, as no default content should be provided."""
2792-
2793- @operation_parameters(
2794- email=TextLine(title=u"Email address."),
2795- password=TextLine(title=u"Password should be at least 8 "
2796- "characters long and contain at least one uppercase letter "
2797- "and a number."),
2798- captcha_id=TextLine(title=u"ID for the generated captcha"),
2799- captcha_solution=TextLine(title=u"Solution for the generated "
2800- "captcha."),
2801- displayname=TextLine(title=u"Full name", required=False))
2802- @export_write_operation()
2803- def register(email, password, captcha_id, captcha_solution, displayname=''):
2804- """ Generate a new captcha """
2805-
2806- @operation_parameters(email=TextLine(title=u"Email address."))
2807- @export_write_operation()
2808- def request_password_reset_token(email):
2809- """Request password reset code to be sent to the email"""
2810-
2811- @operation_parameters(
2812- email=TextLine(title=u"Email address."),
2813- token=TextLine(title=u"Password reset token."),
2814- new_password=TextLine(title=u"New password"))
2815- @export_write_operation()
2816- def set_new_password(email, token, new_password):
2817- """Set new password for given user"""
2818-
2819-
2820-class ICaptchaSet(IDjangoLocation):
2821- export_as_webservice_collection(ICaptcha)
2822-
2823- @collection_default_content()
2824- def getAll():
2825- """Fail, as no default content should be provided."""
2826-
2827- def get(request, unique_id):
2828- """Retrieve a captcha by its ID."""
2829-
2830- @export_write_operation()
2831- def new():
2832- """ Generate a new captcha """
2833
2834=== removed file 'identityprovider/webservice/models.py'
2835--- identityprovider/webservice/models.py 2011-03-14 12:51:02 +0000
2836+++ identityprovider/webservice/models.py 1970-01-01 00:00:00 +0000
2837@@ -1,389 +0,0 @@
2838-# Copyright 2010 Canonical Ltd. This software is licensed under the
2839-# GNU Affero General Public License version 3 (see the file LICENSE).
2840-
2841-from zope.component import getUtility
2842-from zope.interface import implements, classImplements
2843-
2844-from lazr.restful.interfaces import IServiceRootResource
2845-from lazr.restful.simple import (RootResourceAbsoluteURL, RootResource,
2846- TraverseWithGet)
2847-from lazr.restful.frameworks.django import DjangoWebServiceConfiguration
2848-from lazr.restful.wsgi import BaseWSGIWebServiceConfiguration
2849-from lazr.restful.utils import get_current_browser_request
2850-from lazr.restful.declarations import webservice_error
2851-
2852-from oauth_backend.models import Token
2853-from identityprovider.models import (Account, EmailAddress,
2854- AccountPassword, APIUser, Person)
2855-from identityprovider.models.const import (
2856- AccountCreationRationale, AccountStatus, EmailStatus, LoginTokenType)
2857-from identityprovider.models.authtoken import AuthToken, AuthTokenFactory
2858-from identityprovider.utils import encrypt_launchpad_password
2859-from identityprovider.utils import password_policy_compliant
2860-
2861-from identityprovider.webservice.interfaces import (IAccount, ICaptcha,
2862- IRegistration, IValidation, IAuthentication, IAccountSet,
2863- IAuthenticationSet, IRegistrationSet, ICaptchaSet)
2864-from identityprovider.webservice.forms import (
2865- WebserviceCreateAccountForm, PASSWORD_POLICY_ERROR)
2866-from identityprovider.models.captcha import (
2867- Captcha as CaptchaModel,
2868- NewCaptchaError,
2869-)
2870-from identityprovider.views.server import get_team_memberships
2871-from identityprovider.utils import (CannotResetPasswordException,
2872- PersonAndAccountNotFoundException, get_person_and_account_by_email)
2873-from identityprovider.signals import (application_token_invalidated,
2874- application_token_created,
2875- account_email_validated,
2876- account_created)
2877-
2878-
2879-def api_user_required(func):
2880- def wrapper(*args, **kwargs):
2881- request = get_current_browser_request()
2882- user = request.environment.get('authenticated_user')
2883- if user:
2884- if isinstance(user, APIUser):
2885- return func(*args, **kwargs)
2886- request.response.setStatus(403)
2887- return '403 Forbidden'
2888- return wrapper
2889-
2890-
2891-class RootAbsoluteURL(RootResourceAbsoluteURL):
2892- """
2893- This class contains no code of its own. It's defined so that grok will pick
2894- it up.
2895- """
2896-
2897-
2898-# Create set objects for top-level collections, which are all
2899-# pretty much the same.
2900-def make_set(name, url_path, interface, manager, id_field):
2901- """Create a object to publish as a top-level collection."""
2902- def getAll(self):
2903- return []
2904-
2905- def get(self, request, unique_id):
2906- return manager.objects.get(**{id_field: unique_id})
2907-
2908- def __parent__(self):
2909- return getUtility(IServiceRootResource)
2910-
2911- dict = {'getAll': getAll,
2912- 'get': get,
2913- '__parent__': property(__parent__),
2914- '__url_path__': url_path}
2915-
2916- set_class = type(name, (TraverseWithGet,), dict)
2917- classImplements(set_class, interface)
2918- return set_class
2919-
2920-def _serialize_account(user):
2921- emails = EmailAddress.objects.filter(account=user,
2922- status=EmailStatus.VALIDATED)
2923- preferred_email = user.preferredemail
2924- if preferred_email is not None:
2925- preferred_email = preferred_email.email
2926-
2927- if user.person:
2928- username = user.person.name
2929- else:
2930- username = user.openid_identifier
2931-
2932- return {
2933- 'username': username,
2934- 'displayname': user.displayname,
2935- 'openid_identifier': user.openid_identifier,
2936- 'preferred_email': preferred_email,
2937- 'verified_emails': [e.email for e in emails],
2938- 'unverified_emails': [e.email for e in user.unverified_emails()],
2939- }
2940-
2941-
2942-
2943-ValidationSet = make_set('ValidationSet', 'validation', IAccountSet,
2944- Account, 'id')
2945-
2946-
2947-class AccountSet(make_set('BaseAccountSet', 'accounts', IAccountSet,
2948- Account, 'id')):
2949- """ These methods are protected with OAuth."""
2950- def me(self):
2951- request = get_current_browser_request()
2952- user = request.environment.get('authenticated_user')
2953- return _serialize_account(user)
2954-
2955- def team_memberships(self, team_names):
2956- request = get_current_browser_request()
2957- user = request.environment['authenticated_user']
2958- memberships = get_team_memberships(team_names, user, True)
2959- return memberships
2960-
2961- def validate_email(self, email_token):
2962- request = get_current_browser_request()
2963- user = request.environment['authenticated_user']
2964- try:
2965- token = user.authtoken_set.get(
2966- token=email_token, token_type=LoginTokenType.VALIDATEEMAIL)
2967-
2968- email = EmailAddress.objects.get(email__iexact=token.email)
2969- email.status = EmailStatus.VALIDATED
2970- email.save()
2971-
2972- token.consume()
2973-
2974- account_email_validated.send(
2975- openid_identifier=user.openid_identifier,
2976- sender=self)
2977- return {'email': email.email}
2978- except AuthToken.DoesNotExist:
2979- return {'errors': {'email_token': ["Bad email token!"]}}
2980-
2981-
2982-def plain_user_required(func):
2983- def wrapper(*args, **kwargs):
2984- request = get_current_browser_request()
2985- user = request.environment.get('authenticated_user')
2986- if user:
2987- if isinstance(user, Account):
2988- return func(*args, **kwargs)
2989- request.response.setStatus(403)
2990- return '403 Forbidden'
2991- return wrapper
2992-
2993-
2994-class AuthenticationSet(make_set('BaseAuthenticationSet', 'authentications',
2995- IAuthenticationSet, Account, 'id')):
2996- """ All these methods assume that they're run behind Basic Auth """
2997- @plain_user_required
2998- def authenticate(self, token_name):
2999- request = get_current_browser_request()
3000- account = request.environment['authenticated_user']
3001- token = account.create_oauth_token(token_name)
3002- application_token_created.send(
3003- sender=self, openid_identifier=account.openid_identifier)
3004- return token.serialize()
3005-
3006- @api_user_required
3007- def list_tokens(self, consumer_key):
3008- tokens = Token.objects.filter(
3009- consumer__user__username=consumer_key)
3010- result = [{'token': t.token, 'name': t.name} for t in tokens]
3011- return result
3012-
3013- @api_user_required
3014- def validate_token(self, token, consumer_key):
3015- try:
3016- token = Token.objects.get(
3017- consumer__user__username=consumer_key,
3018- token=token)
3019- return token.serialize()
3020- except Token.DoesNotExist:
3021- return False
3022-
3023- @api_user_required
3024- def invalidate_token(self, token, consumer_key):
3025- tokens = Token.objects.filter(token=token,
3026- consumer__user__username=consumer_key)
3027- tokens.delete()
3028- application_token_invalidated.send(
3029- sender=self, openid_identifier=consumer_key)
3030-
3031-
3032- @api_user_required
3033- def team_memberships(self, team_names, openid_identifier):
3034- accounts = Account.objects.filter(openid_identifier=openid_identifier)
3035- accounts = list(accounts)
3036-
3037- if len(accounts) == 1:
3038- account = accounts[0]
3039- memberships = get_team_memberships(team_names, account, False)
3040- return memberships
3041- else:
3042- return []
3043-
3044- @api_user_required
3045- def account_by_email(self, email):
3046- account = Account.objects.get_by_email(email)
3047- if account:
3048- return _serialize_account(account)
3049- else:
3050- return None
3051-
3052- @api_user_required
3053- def account_by_openid(self, openid):
3054- try:
3055- account = Account.objects.get(openid_identifier=openid)
3056- except Account.DoesNotExist:
3057- return None
3058- else:
3059- return _serialize_account(account)
3060-
3061-class CanNotResetPasswordError(Exception):
3062- webservice_error(403)
3063-
3064-
3065-class RegistrationSet(make_set('BaseRegistrationSet', 'registration',
3066- IRegistrationSet, Account, 'id')):
3067- def register(self, **kwargs):
3068- request = get_current_browser_request()
3069- # TODO: request doesn't exist in a test, unless we go through
3070- # the test client browser.
3071- if request is None:
3072- kwargs['remote_ip'] = '127.0.0.1'
3073- else:
3074- if hasattr(request, 'environment'):
3075- kwargs['remote_ip'] = request.environment['REMOTE_ADDR']
3076- else:
3077- kwargs['remote_ip'] = request.environ['REMOTE_ADDR']
3078- form = WebserviceCreateAccountForm(kwargs)
3079- if not form.is_valid():
3080- errors = dict((k, map(unicode, v))
3081- for (k, v) in form.errors.items())
3082- result = {'status': 'error', 'errors': errors}
3083- return result
3084-
3085- cleaned_data = form.cleaned_data
3086- requested_email = cleaned_data['email']
3087- emails = EmailAddress.objects.filter(email__iexact=requested_email)
3088- if len(emails) > 0:
3089- return {'status': 'error', 'errors':
3090- {'email': ['Email already registered']}}
3091-
3092- account = Account.objects.create(
3093- creation_rationale=AccountCreationRationale.OWNER_CREATED_LAUNCHPAD,
3094- status=AccountStatus.ACTIVE,
3095- displayname=cleaned_data.get('displayname', ''))
3096-
3097- account.emailaddress_set.create(
3098- email=cleaned_data['email'],
3099- status=EmailStatus.NEW)
3100-
3101- AccountPassword.objects.create(
3102- password=encrypt_launchpad_password(cleaned_data['password']),
3103- account=account)
3104-
3105- token = AuthTokenFactory().new_api_email_validation_token(
3106- account, cleaned_data['email'])
3107-
3108- token.sendNewUserEmail('api-newuser.txt')
3109-
3110- account_created.send(sender=self,
3111- openid_identifier=account.openid_identifier)
3112-
3113- return {
3114- 'status': 'ok',
3115- 'message': "Email verification required."
3116- }
3117-
3118- def request_password_reset_token(self, email):
3119- error = False
3120- try:
3121- person, account = get_person_and_account_by_email(email)
3122- except (CannotResetPasswordException,
3123- PersonAndAccountNotFoundException):
3124- error = True
3125-
3126- if error or (account is not None and not account.can_reset_password):
3127- raise CanNotResetPasswordError(
3128- "Can't reset password for this account")
3129-
3130- token = AuthTokenFactory().new(account, email, email,
3131- LoginTokenType.PASSWORDRECOVERY, None)
3132-
3133- token.sendPasswordResetEmail()
3134-
3135- return {
3136- 'status': 'ok',
3137- 'message': "Password reset token sent."
3138- }
3139-
3140- def set_new_password(self, email, token, new_password):
3141- token = AuthToken.objects.get(
3142- email=email, token=token,
3143- token_type=LoginTokenType.PASSWORDRECOVERY)
3144- if not token.requester:
3145- token.delete()
3146- return {
3147- 'status': 'error',
3148- 'message': "Wrong token, request new one."
3149- }
3150- if not password_policy_compliant(new_password):
3151- return {
3152- 'status': 'error',
3153- 'errors': [PASSWORD_POLICY_ERROR]
3154- }
3155- password_obj = token.requester.accountpassword
3156- password_obj.password = encrypt_launchpad_password(new_password)
3157- password_obj.save()
3158-
3159- token.consume()
3160-
3161- return {
3162- 'status': 'ok',
3163- 'message': "Password changed"
3164- }
3165-
3166-
3167-class Captcha(object):
3168- implements(ICaptcha)
3169- message = ''
3170- date_created = ''
3171-
3172- @property
3173- def content(self):
3174- return ''
3175-
3176- def __unicode__(self):
3177- return "%s" % (self.id,)
3178-
3179- # IDjangoLocation implementation
3180- @property
3181- def __parent__(self):
3182- from identityprovider.webservice.models import CaptchaSet
3183- return CaptchaSet()
3184-
3185- @property
3186- def __url_path__(self):
3187- return str(self.id)
3188-
3189-
3190-class CaptchaSet(make_set('BaseCaptchaSet', 'captchas', ICaptchaSet,
3191- Captcha, 'id')):
3192- def new(self):
3193- request = get_current_browser_request()
3194- try:
3195- return CaptchaModel.new().serialize()
3196- except NewCaptchaError, e:
3197- request.environment['oops-dump'] = True
3198- logging.warning(e.traceback)
3199- logging.warning("Failed to connect to reCaptcha server")
3200- # TODO: Some better return here?
3201- return e.dummy
3202-
3203-
3204-
3205-class SSOWebServiceRootResource(RootResource):
3206- """The root resource for the web service"""
3207-
3208- def _build_top_level_objects(self):
3209- """Create data structure of top level objects."""
3210- collections = {
3211- 'captchas': (ICaptcha, CaptchaSet()),
3212- 'registration': (IRegistration, RegistrationSet()),
3213- 'validation': (IValidation, ValidationSet()),
3214- 'accounts': (IAccount, AccountSet()),
3215- 'authentications': (IAuthentication, AuthenticationSet()),
3216- }
3217- return collections, {}
3218-
3219-
3220-class WebServiceConfiguration(DjangoWebServiceConfiguration,
3221- BaseWSGIWebServiceConfiguration):
3222- """Configuration information for this web service.
3223-
3224- This class contains no code of its own. It's defined so that grok
3225- will pick it up. The actual configuration is in settings.py.
3226- """
3227
3228=== removed file 'identityprovider/webservice/site.zcml'
3229--- identityprovider/webservice/site.zcml 2010-04-21 15:29:24 +0000
3230+++ identityprovider/webservice/site.zcml 1970-01-01 00:00:00 +0000
3231@@ -1,15 +0,0 @@
3232-<!-- -*- xml -*- -->
3233-<configure xmlns="http://namespaces.zope.org/zope"
3234- xmlns:webservice="http://namespaces.canonical.com/webservice"
3235- xmlns:grok="http://namespaces.zope.org/grok">
3236- <!-- Copyright 2010 Canonical Ltd. This software is licensed under the
3237- GNU Affero General Public License version 3 (see the file LICENSE). -->
3238-
3239- <include package="lazr.restful" file="basic-site.zcml" />
3240- <include package="lazr.restful.frameworks" file="django.zcml" />
3241- <webservice:register module="identityprovider.webservice.interfaces" />
3242- <grok:grok package="identityprovider.webservice" />
3243-
3244- <securityPolicy
3245- component="zope.security.simplepolicies.PermissiveSecurityPolicy" />
3246-</configure>
3247
3248=== modified file 'identityprovider/wsgi.py'
3249--- identityprovider/wsgi.py 2010-12-22 15:23:55 +0000
3250+++ identityprovider/wsgi.py 2011-05-24 16:31:52 +0000
3251@@ -1,90 +1,7 @@
3252 # Copyright 2010 Canonical Ltd. This software is licensed under the
3253 # GNU Affero General Public License version 3 (see the file LICENSE).
3254-import logging
3255-import traceback
3256-
3257 from django.core.handlers.wsgi import WSGIHandler
3258-from django.conf import settings
3259-
3260-from lazr.restful.wsgi import WSGIApplication
3261-from lazr.authentication.wsgi import BasicAuthMiddleware, OAuthMiddleware
3262-from identityprovider.auth import basic_authenticate, oauth_authenticate
3263-from oauth_backend.models import DataStore
3264-
3265-
3266-
3267-class WSGIDispatch(object):
3268- """
3269- Dispatch request to various WSGI applications based on beginning of the
3270- PATH_INFO.
3271-
3272- """
3273- def __init__(self, mapping, default_application):
3274- """
3275- :param mapping: iterable with each item as (url, wsgi application)
3276- :param default_application: application to use if none of urls are
3277- matching
3278- """
3279- self.mapping = mapping
3280- self.default_application = default_application
3281-
3282-
3283- def __call__(self, environ, start_response):
3284- path_info = environ.get('PATH_INFO', '/')
3285- for app_url, app in self.mapping:
3286- if path_info.startswith(app_url):
3287- return app(environ, start_response)
3288- return self.default_application(environ, start_response)
3289-
3290-
3291-
3292-class OopsTracebackReporter(object):
3293-
3294-
3295- def __init__(self, application):
3296- self.application = application
3297-
3298-
3299- def __call__(self, environ, start_response):
3300- try:
3301- return self.application(environ, start_response)
3302- except Exception:
3303- environ['oops-dump'] = True
3304- logging.warning(traceback.format_exc())
3305- start_response("500 Internal Server Error",
3306- [("Content-type", "text/plain")])
3307- return ["An unexpected error occurred while "
3308- "processing an API request"]
3309-
3310-
3311
3312 def make_app():
3313- api_re_prefix = r"/api/[\d.]+/"
3314-
3315 django = WSGIHandler()
3316-
3317- if not getattr(WSGIApplication, 'configured', False):
3318- WSGIApplication.configure_server(settings.API_HOST, None,
3319- 'identityprovider.webservice')
3320- WSGIApplication.configured = True
3321-
3322- lazr_restful = WSGIApplication
3323-
3324- basic = BasicAuthMiddleware(
3325- lazr_restful,
3326- authenticate_with=basic_authenticate,
3327- protect_path_pattern=api_re_prefix + "authentications"
3328- )
3329-
3330- oauth = OAuthMiddleware(
3331- basic,
3332- authenticate_with=oauth_authenticate,
3333- data_store=DataStore(),
3334- protect_path_pattern=api_re_prefix + "accounts"
3335- )
3336-
3337- api = OopsTracebackReporter(oauth)
3338-
3339- dispatch = WSGIDispatch([('/api/1.0', api)], django)
3340-
3341- return dispatch
3342+ return django
3343
3344=== modified file 'mockservice/sso_mockserver/mockserver.py'
3345--- mockservice/sso_mockserver/mockserver.py 2011-03-14 15:55:46 +0000
3346+++ mockservice/sso_mockserver/mockserver.py 2011-05-24 16:31:52 +0000
3347@@ -148,7 +148,7 @@
3348 return
3349
3350 def fail403(self):
3351- status = '403 Forbidden'
3352+ status = '403 FORBIDDEN'
3353 headers = [('Content-type', 'text/plain')]
3354 self.start_response(status, headers)
3355 return
3356@@ -236,17 +236,17 @@
3357 op = form['ws.op']
3358 if op == 'authenticate':
3359 if not self.check_plain_user():
3360- return ['403 Forbidden']
3361+ return ['403 FORBIDDEN']
3362 token = new_token(form['token_name'])
3363 return self.jsons(json=token)
3364 elif op == 'validate_token':
3365 if not self.check_server_user():
3366- return ['403 Forbidden']
3367+ return ['403 FORBIDDEN']
3368 token = tokens[form['token']]
3369 return self.jsons(json=simplejson.dumps(token))
3370 elif op == 'list_tokens':
3371 if not self.check_server_user():
3372- return ['403 Forbidden']
3373+ return ['403 FORBIDDEN']
3374 result = [{'token': t['token'], 'name': t['name']}
3375 for t in tokens.values()
3376 if t['consumer_key'] == form['consumer_key']
3377@@ -276,7 +276,7 @@
3378 op = form['ws.op']
3379 if op == 'invalidate_token':
3380 if not self.check_server_user():
3381- return ['403 Forbidden']
3382+ return ['403 FORBIDDEN']
3383 if form['token'] in tokens:
3384 del tokens[form['token']]
3385 return self.jsons(json='null')
3386
3387=== modified file 'payload/__init__.py'
3388--- payload/__init__.py 2011-05-11 16:21:15 +0000
3389+++ payload/__init__.py 2011-05-24 16:31:52 +0000
3390@@ -47,7 +47,6 @@
3391 _check_psycopg2_conflicts()
3392 # bootstrap
3393 setup_virtualenv()
3394- _bootstrap_dependencies()
3395 install_dependencies()
3396 _setup_configuration()
3397
3398@@ -157,13 +156,6 @@
3399 sys.exit(1)
3400
3401
3402-def _bootstrap_dependencies():
3403- virtualenv('pip install bzr==2.1.1 '
3404- '-f http://launchpad.net/'
3405- 'bzr/2.1/2.1.1/+download/bzr-2.1.1.tar.gz#egg=bzr-2.1.1')
3406- virtualenv('pip install lazr.batchnavigator==1.0')
3407-
3408-
3409 def _setup_configuration():
3410 """Setup the configuration branch."""
3411 if os.path.exists('django_project/.config'):
3412
3413=== modified file 'requirements.txt'
3414--- requirements.txt 2011-03-04 16:49:25 +0000
3415+++ requirements.txt 2011-05-24 16:31:52 +0000
3416@@ -1,56 +1,5 @@
3417-# extra (implicit) dependencies.
3418-# Made explicit in order to pin them down to versions available on Lucid.
3419-RestrictedPython==3.5.1
3420-ZConfig==2.7.1
3421-ZODB3==3.9.4
3422-docutils==0.6
3423-epydoc==3.0.1
3424-grokcore.component==2.0
3425-httplib2==0.6.0
3426-lazr.delegates==1.1.0
3427-lazr.enum==1.1.2
3428-lazr.lifecycle==1.0
3429-lazr.uri==1.0.2
3430-martian==0.12
3431-pytz==2010b
3432-simplejson==2.0.9
3433-storm==0.15
3434-transaction==1.0.0
3435-van.testing==3.0.0
3436-zc.lockfile==1.0.0
3437-zdaemon==2.0.4
3438-zope.app.pagetemplate==3.10.1
3439-zope.authentication==3.7.0
3440-zope.browser==1.2
3441-zope.browserpage==3.11.0
3442-zope.cachedescriptors==3.5.0
3443-zope.configuration==3.7.1
3444-zope.contenttype==3.5.0
3445-zope.copy==3.5.0
3446-zope.datetime>=3.4.0,<=3.4.1dev
3447-zope.dublincore==3.6.0
3448-zope.event==3.4.1
3449-zope.exceptions==3.5.2
3450-zope.i18n==3.7.0
3451-zope.i18nmessageid==3.5.0
3452-zope.lifecycleevent==3.6.0
3453-zope.location==3.8.2
3454-zope.pagetemplate==3.5.0
3455-zope.proxy==3.5.0
3456-zope.publisher==3.10.1
3457-zope.security==3.7.2
3458-zope.size==3.4.1
3459-zope.tal==3.5.2
3460-zope.tales==3.5.0
3461-zope.testing==3.8.3
3462-zope.traversing==3.12.0
3463-
3464 # project dependencies
3465--f http://launchpad.net/lazr.restfulclient/trunk/0.9.11/+download/lazr.restfulclient-0.9.11.tar.gz#egg=lazr.restfulclient-0.9.11
3466 -e bzr+ssh://bazaar.launchpad.net/~canonical-isd-hackers/django-oauth-backend/trunk/@5#egg=django-oauth-backend
3467-# make sure lazr.restful is pinned
3468--f http://launchpad.net/lazr.restful/trunk/0.9.19/+download/lazr.restful-0.9.19.tar.gz#egg=lazr.restful-0.9.19
3469-lazr.restful==0.9.19
3470
3471 # testing dependencies
3472 BeautifulSoup==3.1.0.1
3473@@ -59,10 +8,8 @@
3474 coverage<3.4
3475 mock==0.6.0
3476 wsgiref==0.1.2
3477-lazr.restfulclient==0.9.11
3478 wsgi-intercept==0.4
3479 zope.testbrowser==3.5.1
3480
3481 # development dependencies
3482 werkzeug
3483-
3484
3485=== modified file 'scripts/create_env'
3486--- scripts/create_env 2011-01-03 20:42:20 +0000
3487+++ scripts/create_env 2011-05-24 16:31:52 +0000
3488@@ -24,7 +24,7 @@
3489 url='http://10.55.56.106:8000'
3490 pypi="-i $url"
3491 else
3492- pypi="-f https://launchpad.net/lazr.restful/+download?start=21 -f https://launchpad.net/lazr.restfulclient/+download?start=9"
3493+ pypi="-f https://launchpad.net/lazr.restfulclient/+download?start=9"
3494 fi
3495
3496 virtualenv --distribute --no-site-packages $ENV
3497
3498=== modified file 'setup.py'
3499--- setup.py 2011-05-12 09:18:32 +0000
3500+++ setup.py 2011-05-24 16:31:52 +0000
3501@@ -40,7 +40,6 @@
3502 'coverage==2.85',
3503 'mock==0.6.0',
3504 'wsgiref==0.1.2',
3505- 'lazr.restfulclient==0.9.11',
3506 'wsgi-intercept==0.4',
3507 'zope.testbrowser==3.5.1',
3508 ]
3509@@ -76,8 +75,6 @@
3510 #'django-oauth-backend',
3511 'django-openid-auth==0.2',
3512 'django-preflight==0.1',
3513- 'lazr.authentication==0.1.2',
3514- 'lazr.restful==0.9.19',
3515 # PyPi calls upstream's 1.0a "1.0.1" maybe because it doesn't
3516 # understand letters in version strings. Lucid's package uses "1.0a"
3517 # as upstream does.
3518@@ -85,16 +82,12 @@
3519 'psycopg2==2.0.13',
3520 'python-memcached==1.44',
3521 'python-openid==2.2.4',
3522- 'zope.component==3.8.0',
3523- 'zope.interface==3.5.3',
3524- 'zope.schema==3.6.1',
3525+ 'django-piston',
3526 'gargoyle',
3527 ],
3528 tests_require=tests_require,
3529 extras_require={'test': tests_require},
3530 dependency_links=[
3531 'http://initd.org/pub/software/psycopg/PSYCOPG-2-0/',
3532- 'http://launchpad.net/lazr.restful/trunk/0.9.19/+download/lazr.restful-0.9.19.tar.gz#egg=lazr.restful-0.9.19',
3533- 'http://launchpad.net/lazr.restfulclient/trunk/0.9.11/+download/lazr.restfulclient-0.9.11.tar.gz#egg=lazr.restfulclient-0.9.11',
3534 ],
3535 )