Merge lp:~fgallina/rnr-server/ssov2 into lp:rnr-server

Proposed by Fabián Ezequiel Gallina on 2014-07-16
Status: Merged
Approved by: Fabián Ezequiel Gallina on 2014-07-18
Approved revision: 256
Merged at revision: 252
Proposed branch: lp:~fgallina/rnr-server/ssov2
Merge into: lp:rnr-server
Diff against target: 1934 lines (+801/-596)
13 files modified
.bzrignore (+4/-0)
django_project/config_dev/config/main.cfg (+10/-8)
fabtasks/bootstrap.py (+21/-7)
src/clickreviews/tests/test_utilities.py (+1/-1)
src/reviewsapp/auth.py (+135/-112)
src/reviewsapp/forms.py (+4/-5)
src/reviewsapp/preflight.py (+6/-10)
src/reviewsapp/schema.py (+6/-4)
src/reviewsapp/tests/factory.py (+26/-0)
src/reviewsapp/tests/test_auth.py (+374/-140)
src/reviewsapp/tests/test_rnrclient.py (+129/-166)
src/reviewsapp/tests/test_utilities.py (+45/-53)
src/reviewsapp/utilities.py (+40/-90)
To merge this branch: bzr merge lp:~fgallina/rnr-server/ssov2
Reviewer Review Type Date Requested Status
Matias Bordese 2014-07-16 Approve on 2014-07-18
Review via email: mp+227058@code.launchpad.net

Commit message

Migrate to SSO V2 API

To post a comment you must log in.
Matias Bordese (matiasb) wrote :

Added inline comments.
Also, for future reference/next MP, removing oauthtoken models :-)
https://code.launchpad.net/~nataliabidart/software-center-agent/no-oauthtoken-model/+merge/226944

lp:~fgallina/rnr-server/ssov2 updated on 2014-07-17
254. By Fabián Ezequiel Gallina on 2014-07-17

reviewsapp.auth: Do not check for token freshness

255. By Fabián Ezequiel Gallina on 2014-07-17

Update user data from SSO when authenticating

Also includes changes to make web_services.get_data_for_account more
robust.

Matias Bordese (matiasb) wrote :

Added a couple of minor comments.

Also attaching chat about verified accounts and getting SSO account details on auth (conclusion: you are good with your updates in the MP).

(16:20:55) matiasb: beuno: so, a couple of questions re RnR; should we allow unverified users to rate/review? I guess we should? and also, originally, RnR keeps user details (full name, email) updated from SSO... should that be preserved in the migration? (it requires 2 SSO API calls for each RnR API request)
(16:37:55) beuno: matiasb, I think we shouldn't
(16:38:05) beuno: it's what would be most prone to abuse, no?
(16:38:21) beuno: as for keeping user details
(16:38:23) matiasb: yeah, that's right
(16:38:27) beuno: not sure, why would it need to?
(16:39:16) matiasb: to keep the user's details up to date according to sso possible changes
(16:39:40) matiasb: that's what it is doing right now, but I wouldn't
(16:39:55) matiasb: but checking, since fabian is working on that in the MP above
(16:40:33) beuno: matiasb, so it doesn't have to call back out to SSO for them?
(16:41:13) beuno: I sort of feel we should always just call out to SSO, with some level of caching on the clients and server
(16:41:15) matiasb: beuno: it calls back SSO when creating the user, but now it is calling SSO when validating the request and then to get updated account details
(16:41:51) matiasb: beuno: right, with some delta, but I guess no every time
(16:42:10) beuno: matiasb, yeah, but I think it would be a memcache-y thing
(16:42:15) beuno: rather than a DB thing
(16:42:30) beuno: and make sure each service deals with not having that data in a reasonable way
(16:42:39) matiasb: makes sense
(16:42:45) beuno: so have the service cache that data for, say, 30 minutes
(16:42:55) beuno: and SSO cache it until it's invalidated or something like that
(16:43:03) beuno: so it's mostly memcaches talking to memcaches
(16:43:07) matiasb: heh
(16:43:19) beuno: EVERYTHING IN RAM!
(16:43:30) beuno: like the SCA testsuite
(16:43:41) matiasb: heh :-/
(16:44:01) matiasb: agree, sounds good to me
(16:44:15) matiasb: we should get that in place too then
(16:44:50) beuno: matiasb, I may regrest suggesting this
(16:44:54) beuno: but until we do
(16:45:03) beuno: I think we just ask SSO for those details each time
(16:45:14) matiasb: ack, got it

Fabián Ezequiel Gallina (fgallina) wrote :

Pushed fixes and replied comment.

lp:~fgallina/rnr-server/ssov2 updated on 2014-07-18
256. By Fabián Ezequiel Gallina on 2014-07-18

Removed sso_token_max_age obsolete setting from schema

Matias Bordese (matiasb) wrote :

+1, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2014-01-07 09:02:48 +0000
3+++ .bzrignore 2014-07-18 11:18:53 +0000
4@@ -15,6 +15,10 @@
5 django_project/oops_dictconfig
6 django_project/oops_wsgi
7 django_project/adminaudit
8+django_project/oauthlib
9+django_project/requests
10+django_project/requests_oauthlib
11+django_project/ssoclient
12 tags
13 .backup
14 django_project/local.cfg
15
16=== modified file 'django_project/config_dev/config/main.cfg'
17--- django_project/config_dev/config/main.cfg 2014-01-15 10:10:22 +0000
18+++ django_project/config_dev/config/main.cfg 2014-07-18 11:18:53 +0000
19@@ -130,14 +130,16 @@
20 modify_window_minutes = 120
21
22 [sso_api]
23-sso_api_service_root = https://login.staging.ubuntu.com/api/1.0
24-sso_api_auth_username = replace_with_your_sso_apiuser
25-sso_api_auth_password = replace_with_your_sso_apipassword
26-# this option will disable talking to the sso_api and instead extract
27-# token_secret and consumer_secret from the PLAINTEXT signature.
28-# This avoids the need for a special ubuntu-sso API account
29-# and is very useful for local testing
30-sso_auth_mode_no_ubuntu_sso_plaintext_only = False
31+sso_api_service_root = https://login.staging.ubuntu.com/api/v2
32+sso_v2_api_token_data = sso_api_token_data
33+# Four hours
34+sso_token_max_age = 14400
35+
36+[sso_api_token_data]
37+token_key =
38+token_secret =
39+consumer_key =
40+consumer_secret =
41
42 [launchpad]
43 lp_service = production
44
45=== modified file 'fabtasks/bootstrap.py'
46--- fabtasks/bootstrap.py 2014-01-07 09:02:48 +0000
47+++ fabtasks/bootstrap.py 2014-07-18 11:18:53 +0000
48@@ -62,13 +62,6 @@
49 "django_project/rnrclient.py")
50
51 _get_or_pull_bzr_branch(
52- "lp:~canonical-isd-hackers/canonical-identity-provider/trunk",
53- "canonical-identity-provider")
54- _symlink(
55- "branches/canonical-identity-provider/src/mockservice/sso_mockserver",
56- "django_project/mockssoservice")
57-
58- _get_or_pull_bzr_branch(
59 "lp:~ubuntuone-pqm-team/python-oops-wsgi/stable",
60 "oops-wsgi",
61 revision=36)
62@@ -92,6 +85,27 @@
63 "django_project/scaclient.py")
64
65 _get_or_pull_bzr_branch(
66+ "lp:~ubuntuone-pqm-team/oauthlib/stable", "oauthlib", revision="0.6.3")
67+ _symlink("branches/oauthlib/oauthlib", "django_project/oauthlib")
68+
69+ _get_or_pull_bzr_branch(
70+ "lp:~ubuntuone-pqm-team/requests/stable", "requests", revision="2.2.1")
71+ _symlink("branches/requests/requests", "django_project/requests")
72+
73+ _get_or_pull_bzr_branch(
74+ "lp:~ubuntuone-pqm-team/requests-oauthlib/stable",
75+ "requests-oauthlib", revision=16)
76+ _symlink("branches/requests-oauthlib/requests_oauthlib",
77+ "django_project/requests_oauthlib")
78+
79+ _get_or_pull_bzr_branch(
80+ "lp:~canonical-isd-hackers/canonical-identity-provider/ssoclient",
81+ "ssoclient", revision=18)
82+ _symlink(
83+ "branches/ssoclient/ssoclient",
84+ "django_project/ssoclient")
85+
86+ _get_or_pull_bzr_branch(
87 "lp:txstatsd",
88 "txstatsd")
89 _symlink(
90
91=== modified file 'src/clickreviews/tests/test_utilities.py'
92--- src/clickreviews/tests/test_utilities.py 2014-01-10 10:02:30 +0000
93+++ src/clickreviews/tests/test_utilities.py 2014-07-18 11:18:53 +0000
94@@ -8,7 +8,7 @@
95 class VerifyPackageTestCase(unittest.TestCase):
96
97 def patch_httplib2_request(self):
98- patcher = patch('reviewsapp.utilities.Http.request')
99+ patcher = patch('clickreviews.utilities.Http.request')
100 self.addCleanup(patcher.stop)
101 return patcher.start()
102
103
104=== modified file 'src/reviewsapp/auth.py'
105--- src/reviewsapp/auth.py 2014-06-23 20:19:03 +0000
106+++ src/reviewsapp/auth.py 2014-07-18 11:18:53 +0000
107@@ -23,24 +23,113 @@
108 __all__ = [
109 'SSOOAuthAuthentication',
110 ]
111-import pytz
112-
113-from datetime import datetime, timedelta
114-from django.conf import settings
115+
116+import logging
117+import sys
118+
119+from django.db import IntegrityError, transaction
120 from django.contrib.auth.models import User
121 from django.http import HttpResponse
122+from django.utils import six
123 from oauth import oauth
124-from piston.authentication import OAuthAuthentication, oauth_datastore
125-from piston.oauth import OAuthError
126+from piston.authentication import OAuthAuthentication
127
128-from reviewsapp.models import Consumer, Token
129 from reviewsapp.utilities import (
130- WebServices,
131 full_claimed_id,
132+ web_services,
133 )
134
135-TOKEN_CACHE_EXPIRY = timedelta(
136- hours=getattr(settings, 'TOKEN_CACHE_EXPIRY_HOURS', 4))
137+logger = logging.getLogger(__name__)
138+
139+
140+def _get_available_username(username):
141+ """Return a valid and available username."""
142+ # follow similar approach to django_openid_auth and web login
143+ i = User.objects.filter(username__startswith=username).count() + 1
144+ while True:
145+ if i > 1:
146+ username += str(i)
147+ try:
148+ User.objects.get(username__exact=username)
149+ except User.DoesNotExist:
150+ break
151+ i += 1
152+ return username
153+
154+
155+@transaction.commit_on_success
156+def update_or_create_user_from_oauth(consumer_key):
157+ """Update or create user using SSO account data.
158+
159+ This function tries to retrieve user data from SSO using
160+ consumer_key. In case user data cannot be retrieved, the
161+ consumer_key is used to lookup for an existing `User` instance to
162+ be returned. In the case the user data from SSO is retrieved
163+ correctly and a user for the given consumer_key already exists,
164+ the data for the user is updated with it. Finally, if the user
165+ does not exist, it's created with the retrieved SSO data.
166+
167+ Returns:
168+ A two-element tuple, where the first element is the
169+ created/updated user or None and the last one is a boolean
170+ especifying whether the user was created or not."""
171+ claimed_id = full_claimed_id(consumer_key)
172+ created = False
173+ first_name = last_name = email = ""
174+ account = web_services.get_data_for_account(
175+ openid_identifier=consumer_key)
176+
177+ try:
178+ user = User.objects.get(useropenid__claimed_id=claimed_id)
179+ except User.DoesNotExist:
180+ user = None
181+
182+ valid_account = (account is not None and
183+ account.get('verified') and
184+ account.get('email') and
185+ account.get('displayname') is not None)
186+
187+ if not valid_account:
188+ logger.warn('OAuthRequest was valid but could not get user data: %s',
189+ consumer_key)
190+ else:
191+ email = account['email']
192+ first_name, _, last_name = account['displayname'].rpartition(" ")
193+
194+ if user is not None:
195+ if valid_account:
196+ user.first_name = first_name
197+ user.last_name = last_name
198+ user.email = email
199+ user.save()
200+ elif valid_account:
201+ created = True
202+ username = _get_available_username(consumer_key)
203+ # mimic django get_or_create, handling a possible IntegrityError
204+ # if the user was created in the meantime
205+ try:
206+ sid = transaction.savepoint()
207+ user = User.objects.create_user(username, email, password=None)
208+ user.first_name = first_name
209+ user.last_name = last_name
210+ user.save()
211+ user.useropenid_set.create(
212+ claimed_id=claimed_id, display_id=claimed_id)
213+ transaction.savepoint_commit(sid)
214+ except IntegrityError:
215+ transaction.savepoint_rollback(sid)
216+ exc_info = sys.exc_info()
217+ try:
218+ user = User.objects.get(useropenid__claimed_id=claimed_id)
219+ except User.DoesNotExist:
220+ # Re-raise the IntegrityError with its original traceback.
221+ six.reraise(*exc_info)
222+ else:
223+ # no valid_account nor user, this clause is noop but explicit
224+ # is better than implicit.
225+ user, created = None, False
226+
227+ return user, created
228
229
230 class SSOOAuthAuthentication(OAuthAuthentication):
231@@ -62,108 +151,42 @@
232 return resp
233
234 def is_authenticated(self, request):
235- """ We override is_authenticated so we can prefetch creds from SSO
236- before the oauth mechanism asks us for them
237- """
238- headers = {
239- 'Authorization': request.META.get('HTTP_AUTHORIZATION', '')
240+ authorization = request.META.get('HTTP_AUTHORIZATION', None)
241+ query_string = request.META.get('QUERY_STRING', None)
242+
243+ if not authorization and not query_string:
244+ return False
245+
246+ auth_data = {
247+ 'http_url': request.build_absolute_uri(),
248+ 'http_method': request.method,
249+ 'authorization': authorization,
250+ 'query_string': query_string,
251 }
252- orequest = oauth.OAuthRequest.from_request(
253- request.method, request.build_absolute_uri(), headers=headers,
254- query_string=request.META['QUERY_STRING'])
255- if (orequest is None or
256- 'oauth_token' not in orequest.parameters or
257- 'oauth_consumer_key' not in orequest.parameters):
258+
259+ # check this is a valid SSO OAuth request
260+ success = web_services.validate_request(data=auth_data)
261+
262+ headers = {}
263+ if authorization:
264+ headers = {'Authorization': authorization}
265+
266+ if not success:
267 return False
268
269- self.prefetch_oauth_consumer(orequest)
270- if self.is_valid_request(request):
271- try:
272- consumer, token, parameters = self.validate_token(request)
273- except OAuthError:
274- return False
275- if consumer and token:
276- request.user = token.user
277- request.throttle_extra = token.consumer.id
278- return True
279- return False
280-
281- def prefetch_oauth_consumer(self, request):
282- """Check if we have a consumer for the current creds, and validate
283- the token with SSO if we don't.
284- """
285- web_services = WebServices()
286- oauthtoken = request.get_parameter('oauth_token')
287- consumer_key = request.get_parameter('oauth_consumer_key')
288- tokens = Token.objects.filter(token=oauthtoken,
289- consumer__key=consumer_key)
290- if len(tokens) == 0 or (
291- tokens[0].updated_at < (
292- datetime.now(pytz.utc) - TOKEN_CACHE_EXPIRY)):
293- pieces = web_services.get_data_for_account(
294- token=oauthtoken,
295- openid_identifier=consumer_key,
296- signature=request.get_parameter('oauth_signature')
297- )
298- if not pieces:
299- return
300- Consumer.objects.filter(key=consumer_key).exclude(
301- secret=pieces['consumer_secret']).delete()
302- claimed_id = full_claimed_id(consumer_key)
303- try:
304- user = User.objects.get(
305- useropenid__claimed_id=claimed_id)
306- except User.DoesNotExist:
307- if (not pieces.get('preferred_email') or
308- not pieces.get('username')):
309- return
310- user = User.objects.create_user(pieces['username'],
311- pieces['preferred_email'],
312- password=None)
313- user.useropenid_set.create(claimed_id=claimed_id)
314-
315- displayname = pieces['displayname']
316- if displayname != user.get_full_name():
317- user.first_name, sep, user.last_name = displayname.rpartition(
318- ' ')
319- user.save()
320-
321- consumer, created = Consumer.objects.get_or_create(
322- user=user, key=consumer_key,
323- secret=pieces['consumer_secret'])
324- token, created = Token.objects.get_or_create(
325- consumer=consumer, token=pieces['token'],
326- token_secret=pieces['token_secret'], name=pieces['name'])
327- if not created:
328- # Update updated_at
329- token.save()
330-
331- @staticmethod
332- def validate_token(request, check_timestamp=True, check_nonce=True):
333- oauth_server, oauth_request = initialize_server_request(request)
334- if oauth_server is None:
335- raise OAuthError('initialize_server_request returned None')
336- return oauth_server.verify_request(oauth_request)
337-
338-
339-def initialize_server_request(request):
340- """
341- Shortcut for initialization.
342- """
343- headers = {
344- 'Authorization': request.META.get('HTTP_AUTHORIZATION', '')
345- }
346- oauth_request = oauth.OAuthRequest.from_request(
347- request.method, request.build_absolute_uri(), headers=headers,
348- query_string=request.META['QUERY_STRING'])
349-
350- if oauth_request:
351- oauth_server = oauth.OAuthServer(oauth_datastore(oauth_request))
352- oauth_server.add_signature_method(
353- oauth.OAuthSignatureMethod_PLAINTEXT())
354- oauth_server.add_signature_method(
355- oauth.OAuthSignatureMethod_HMAC_SHA1())
356- else:
357- oauth_server = None
358-
359- return oauth_server, oauth_request
360+ oauth_request = oauth.OAuthRequest.from_request(
361+ http_method=auth_data['http_method'],
362+ http_url=auth_data['http_url'],
363+ headers=headers,
364+ query_string=auth_data.get('query_string', None),
365+ )
366+
367+ openid = oauth_request.get_parameter('oauth_consumer_key')
368+
369+ user, _ = update_or_create_user_from_oauth(openid)
370+ if user is not None:
371+ request.user = user
372+ else:
373+ success = False
374+
375+ return success
376
377=== modified file 'src/reviewsapp/forms.py'
378--- src/reviewsapp/forms.py 2014-02-12 09:09:56 +0000
379+++ src/reviewsapp/forms.py 2014-07-18 11:18:53 +0000
380@@ -41,7 +41,7 @@
381 ReviewModeration,
382 SoftwareItem,
383 )
384-from reviewsapp.utilities import WebServices
385+from reviewsapp.utilities import web_services
386 from reviewsapp.widgets import MultipleSubmitButton
387
388
389@@ -166,14 +166,13 @@
390 require_repository_check = True
391
392 if require_repository_check:
393- ws = WebServices()
394- is_valid_package = ws.verify_packagename_in_repository(
395+ is_valid_package = web_services.verify_packagename_in_repository(
396 package_name, origin, distroseries, arch_tag=arch_tag)
397
398 if not is_valid_package:
399- valid_in_sca = ws.sca_verify_packagename_in_repository(
400+ in_sca = web_services.sca_verify_packagename_in_repository(
401 package_name, origin, distroseries, arch_tag=arch_tag)
402- if not valid_in_sca:
403+ if not in_sca:
404 raise forms.ValidationError(
405 ': package {0} not in {1} {2} for {2}'.format(
406 package_name, origin, distroseries, arch_tag))
407
408=== modified file 'src/reviewsapp/preflight.py'
409--- src/reviewsapp/preflight.py 2011-03-14 14:54:09 +0000
410+++ src/reviewsapp/preflight.py 2014-07-18 11:18:53 +0000
411@@ -30,7 +30,7 @@
412 from preflight import Preflight, register
413
414 import reviewsapp
415-from .utilities import WebServices
416+from reviewsapp.utilities import web_services
417
418
419 class RNRPreflight(Preflight):
420@@ -52,13 +52,9 @@
421 Make sure that the access to the Ubuntu SSO API is correct.
422
423 """
424- sso = WebServices().identity_provider
425- # Getting list of tokens for wrong customer should result with an
426- # empty list. If anything goes wrong then api will raise an
427- # exception (like: HTTP Error 401: Unauthorized)
428- tokens = sso.authentications.list_tokens(
429- consumer_key='wrong-consumer-key')
430- return len(tokens) == 0
431+ sso = web_services.ssoclient
432+ open_id = settings.SSO_V2_API_TOKEN_DATA['consumer_key']
433+ return sso.account_details(open_id)
434
435 def check_launchpad_api_access(self):
436 """
437@@ -66,7 +62,7 @@
438
439 """
440 # Pull one data item from Launchpad
441- bug = WebServices().launchpad_service.bugs[1]
442+ bug = web_services.launchpad_service.bugs[1]
443 return bug.id == 1
444
445 def check_validate_config(self):
446@@ -77,7 +73,7 @@
447 return parser.is_valid()
448
449 def check_sca_validation(self):
450- pubs = WebServices().get_sca_publishings_for('maverick', 'i386')
451+ pubs = web_services.get_sca_publishings_for('maverick', 'i386')
452 return type(pubs) == dict
453
454
455
456=== modified file 'src/reviewsapp/schema.py'
457--- src/reviewsapp/schema.py 2014-01-03 11:11:18 +0000
458+++ src/reviewsapp/schema.py 2014-07-18 11:18:53 +0000
459@@ -72,10 +72,12 @@
460 # SSO API
461 class sso_api(schema.Section):
462 sso_api_service_root = schema.StringOption()
463- sso_api_auth_username = schema.StringOption()
464- sso_api_auth_password = schema.StringOption()
465- sso_api_identity_prefix = schema.StringOption()
466- sso_auth_mode_no_ubuntu_sso_plaintext_only = schema.BoolOption()
467+ sso_v2_api_token_data = schema.DictOption(spec={
468+ 'token_key': schema.StringOption(raw=True),
469+ 'token_secret': schema.StringOption(raw=True),
470+ 'consumer_key': schema.StringOption(raw=True),
471+ 'consumer_secret': schema.StringOption(raw=True),
472+ }, strict=True)
473
474 # RnR
475 class rnr(schema.Section):
476
477=== modified file 'src/reviewsapp/tests/factory.py'
478--- src/reviewsapp/tests/factory.py 2014-06-23 20:12:26 +0000
479+++ src/reviewsapp/tests/factory.py 2014-07-18 11:18:53 +0000
480@@ -36,6 +36,12 @@
481 TransactionTestCase,
482 )
483 from django_openid_auth.models import UserOpenID
484+from oauth.oauth import (
485+ OAuthRequest,
486+ OAuthConsumer,
487+ OAuthToken,
488+ OAuthSignatureMethod_PLAINTEXT
489+)
490 from testtools import TestCase as TestToolsTestCase
491
492 from reviewsapp.models import (
493@@ -266,6 +272,26 @@
494
495 return review_moderation
496
497+ def _get_signed_oauth_request(self, request, token, consumer):
498+ consumer = OAuthConsumer(consumer.key, consumer.secret)
499+ token = OAuthToken(token.token, token.token_secret)
500+ oauth_request = OAuthRequest.from_consumer_and_token(
501+ consumer, token, http_url=request.build_absolute_uri())
502+ oauth_request.sign_request(OAuthSignatureMethod_PLAINTEXT(),
503+ consumer, token)
504+ return oauth_request
505+
506+ def header_from_token(self, request, token, consumer):
507+ oauth_request = self._get_signed_oauth_request(request, token,
508+ consumer)
509+ headers = oauth_request.to_header('Ratings and Reviews')
510+ return {'HTTP_AUTHORIZATION': headers['Authorization']}
511+
512+ def querystring_from_token(self, request, token, consumer):
513+ oauth_request = self._get_signed_oauth_request(request, token,
514+ consumer)
515+ return oauth_request.to_postdata()
516+
517
518 class TestCaseWithFactory(TestCase, TestToolsTestCase):
519
520
521=== modified file 'src/reviewsapp/tests/test_auth.py'
522--- src/reviewsapp/tests/test_auth.py 2012-06-20 12:19:49 +0000
523+++ src/reviewsapp/tests/test_auth.py 2014-07-18 11:18:53 +0000
524@@ -21,26 +21,24 @@
525 __metaclass__ = type
526 __all__ = [
527 'SSOOAuthAuthenticationTestCase',
528+ 'UpdateOrCreateUserFromOauthTestCase',
529 ]
530
531 from django.conf import settings
532 from django.contrib.auth.models import User
533 from django.http import HttpResponse
534 import httplib
535-from mock import patch
536-from oauth.oauth import (
537- OAuthConsumer,
538- OAuthRequest,
539- OAuthSignatureMethod_PLAINTEXT,
540- OAuthToken,
541-)
542+from mock import Mock, patch
543 from piston.resource import Resource
544 from piston.handler import BaseHandler
545
546-from reviewsapp.auth import SSOOAuthAuthentication
547-from reviewsapp.models import Consumer
548+from reviewsapp.auth import (
549+ SSOOAuthAuthentication,
550+ full_claimed_id,
551+ update_or_create_user_from_oauth,
552+)
553 from reviewsapp.tests.factory import TestCaseWithFactory
554-from reviewsapp.tests.helpers import mock_data_for_account
555+from reviewsapp.utilities import web_services
556
557
558 class MockRequest:
559@@ -72,7 +70,35 @@
560 return HttpResponse(status=httplib.CREATED)
561
562
563-class SSOOAuthAuthenticationTestCase(TestCaseWithFactory):
564+class BaseAuthTestCase(TestCaseWithFactory):
565+
566+ def make_request_with_token(self, save=True, method='GET'):
567+ token, consumer = self.factory.makeOAuthTokenAndConsumer(save=save)
568+ request = MockRequest(method=method)
569+ request.META.update(self.factory.header_from_token(request, token,
570+ consumer))
571+ return request, token, consumer
572+
573+ def make_request_with_querystring_token(self, method='GET'):
574+ token, consumer = self.factory.makeOAuthTokenAndConsumer(save=False)
575+ request = MockRequest(method=method)
576+ qs = self.factory.querystring_from_token(request, token, consumer)
577+ request.META.update({'QUERY_STRING': qs})
578+ return request, token, consumer
579+
580+ def mock_data_for_account(self, consumer=None, **kwargs):
581+ """ A reasonable return value for get_data_for_account. """
582+ data = {
583+ 'email': 'foo@bar.com',
584+ 'verified': True,
585+ 'displayname': 'Foo',
586+ }
587+ data.update(kwargs)
588+ return data
589+
590+
591+class SSOOAuthAuthenticationTestCase(BaseAuthTestCase):
592+
593 def setUp(self):
594 self.auth = SSOOAuthAuthentication(realm="Ratings and Reviews")
595 self.get_resource = Resource(MockGetHandler,
596@@ -81,51 +107,50 @@
597 authentication=self.auth)
598 super(SSOOAuthAuthenticationTestCase, self).setUp()
599
600- def header_from_token(self, request, token, consumer):
601- consumer = OAuthConsumer(consumer.key, consumer.secret)
602- token = OAuthToken(token.token, token.token_secret)
603- oauth_request = OAuthRequest.from_consumer_and_token(
604- consumer, token, http_url=request.build_absolute_uri())
605- oauth_request.sign_request(OAuthSignatureMethod_PLAINTEXT(),
606- consumer, token)
607- headers = oauth_request.to_header('Ratings and Reviews')
608- return {'HTTP_AUTHORIZATION': headers['Authorization']}
609+ p = patch('reviewsapp.auth.web_services')
610+ self.mock_web_services = p.start()
611+ self.addCleanup(p.stop)
612+
613+ self.mock_get_data = self.mock_web_services.get_data_for_account
614+ self.mock_validate = self.mock_web_services.validate_request
615+ self.mock_validate.return_value = True
616+ self.mock_get_data.return_value = self.mock_data_for_account()
617+
618+ web_services.refresh_connections()
619
620 def test_no_auth_fails(self):
621+ request = MockRequest()
622+ assert not request.META.get('HTTP_AUTHORIZATION')
623+ assert not request.META.get('QUERY_STRING')
624
625- response = self.get_resource(MockRequest())
626+ response = self.get_resource(request)
627
628 self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
629+ self.assertFalse(self.mock_validate.called)
630
631- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
632- def test_success(self, mock_get_data):
633+ def test_success(self):
634 request, token, consumer = self.make_request_with_token()
635
636 response = self.get_resource(request)
637
638- self.assertFalse(mock_get_data.called)
639+ auth_data = {
640+ 'http_url': request.build_absolute_uri(),
641+ 'http_method': request.method,
642+ 'authorization': request.META.get('HTTP_AUTHORIZATION', None),
643+ 'query_string': request.META.get('QUERY_STRING', None),
644+ }
645+ self.mock_validate.assert_called_with(data=auth_data)
646 self.assertEqual(httplib.OK, response.status_code)
647
648- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
649- def test_fetch_creds_from_sso(self, mock_get_data):
650+ def test_fetch_creds_from_sso(self):
651 request, token, consumer = self.make_request_with_token(save=False)
652- mock_get_data.return_value = mock_data_for_account(consumer, token)
653+ self.mock_get_data.return_value = self.mock_data_for_account(consumer)
654
655 response = self.get_resource(request)
656
657- self.assertTrue(mock_get_data.called)
658+ self.assertTrue(self.mock_validate.called)
659 self.assertEqual(httplib.OK, response.status_code)
660
661- @patch('reviewsapp.auth.initialize_server_request')
662- def test_initialize_server_request_fails(self, mock_init_request):
663- """Server shouldn't return a 500 if initialize_server_request fails"""
664- mock_init_request.return_value = None, None
665- request, token, consumer = self.make_request_with_token()
666-
667- response = self.get_resource(request)
668-
669- self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
670-
671 def test_post_data_doesnt_replace_parameters(self):
672 """Check that POST data doesn't replace information from
673 the Authentication header in the OAuthRequest parameter set.
674@@ -139,53 +164,55 @@
675
676 self.assertEqual(httplib.CREATED, response.status_code)
677
678- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
679- def test_unknown_token(self, mock_get_data):
680- """Check that if the consumer is known, but a new token is provided
681- the data is fetched from SSO
682- """
683- request, token, consumer = self.make_request_with_token(method='POST')
684- account_data = mock_data_for_account(consumer, token)
685- orig_plaintext_sig = "%s&%s" % (
686- account_data['consumer_secret'], account_data['token_secret'])
687- mock_get_data.return_value = account_data
688- request.POST = request.REQUEST = {'foo': 'bar', 'baz': 'troz'}
689- orig_token = token.token
690- token.delete()
691-
692- response = self.post_resource(request)
693-
694- mock_get_data.assert_called_with(
695- token=orig_token, openid_identifier=consumer.key,
696- signature=orig_plaintext_sig)
697- self.assertEqual(httplib.CREATED, response.status_code)
698-
699- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
700- def test_user_doesnt_exist(self, mock_get_data):
701+ def test_user_doesnt_exist(self):
702 """Check that a user is created if none exists"""
703 request, token, consumer = self.make_request_with_token(save=False)
704- data_for_account = mock_data_for_account(consumer, token)
705- mock_get_data.return_value = data_for_account
706+ data_for_account = self.mock_data_for_account(consumer)
707+ self.mock_get_data.return_value = data_for_account
708 consumer.user.delete()
709- request = MockRequest()
710- request.META.update(self.header_from_token(request, token, consumer))
711 new_user = self.factory.makeUser()
712
713- patch_path = 'django.contrib.auth.models.UserManager.create_user'
714- with patch(patch_path) as mock_create_user:
715+ create_method = 'django.contrib.auth.models.UserManager.create_user'
716+ with patch(create_method) as mock_create_user:
717 mock_create_user.return_value = new_user
718 response = self.get_resource(request)
719
720 mock_create_user.assert_called_with(
721- data_for_account['username'], data_for_account['preferred_email'],
722- password=None)
723+ consumer.key, data_for_account['email'], password=None)
724 self.assertEqual(httplib.OK, response.status_code)
725
726- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
727- def test_user_doesnt_exist_twice(self, mock_get_data):
728+ def test_user_is_not_authenticated(self):
729+ request, token, consumer = self.make_request_with_token(save=False)
730+ data_for_account = self.mock_data_for_account(consumer)
731+ self.mock_get_data.return_value = data_for_account
732+ consumer.user.delete()
733+ self.factory.makeUser()
734+
735+ get_method = 'reviewsapp.auth.update_or_create_user_from_oauth'
736+ with patch(get_method) as mock_get_user:
737+ mock_get_user.return_value = (None, False)
738+ response = self.get_resource(request)
739+
740+ self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
741+
742+ def test_invalid_token(self):
743+ """Check that a 401 is returned if an invalid token is provided.
744+
745+ This test doesn't mock SSOOAuthAuthentication.validate_token to
746+ trigger OAuthError explicitly, to ensure that
747+ whatever exception gets raised it is caught and handled.
748+ """
749+ request, token, consumer = self.make_request_with_token(save=False)
750+ self.mock_validate.return_value = False
751+
752+ response = self.get_resource(request)
753+
754+ self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
755+
756+ def test_user_doesnt_exist_twice(self):
757 """Check that an autocreated user persists on for a second call"""
758 request, token, consumer = self.make_request_with_token(save=False)
759- mock_get_data.return_value = mock_data_for_account(consumer, token)
760+ self.mock_get_data.return_value = self.mock_data_for_account(consumer)
761 consumer.user.delete()
762 new_user = self.factory.makeUser()
763 create_user = 'django.contrib.auth.models.UserManager.create_user'
764@@ -194,21 +221,19 @@
765 # First request...
766 response = self.get_resource(request)
767 request = MockRequest()
768- request.META.update(
769- self.header_from_token(request, token, consumer))
770+ request.META.update(self.factory.header_from_token(request, token,
771+ consumer))
772
773 response = self.get_resource(request)
774
775 self.assertEqual(1, mock_create_user.call_count)
776- self.assertEqual(1, mock_get_data.call_count)
777 self.assertEqual(httplib.OK, response.status_code)
778
779- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
780- def test_user_is_created_with_right_claimed_id(self, mock_get_data):
781+ def test_user_is_created_with_right_id(self):
782 """Check that autocreated users get the right (full) claimed_id"""
783 request, token, consumer = self.make_request_with_token(save=False)
784- data_for_account = mock_data_for_account(consumer, token)
785- mock_get_data.return_value = data_for_account
786+ data_for_account = self.mock_data_for_account(consumer)
787+ self.mock_get_data.return_value = data_for_account
788 consumer.user.delete()
789 new_user = self.factory.makeUser()
790 new_user.useropenid_set.all().delete()
791@@ -216,79 +241,288 @@
792 create_user = 'django.contrib.auth.models.UserManager.create_user'
793 with patch(create_user) as mock_create_user:
794 mock_create_user.return_value = new_user
795- response = self.get_resource(request)
796+ self.get_resource(request)
797
798 self.assertEqual(1, new_user.useropenid_set.count())
799- claimed_id = new_user.useropenid_set.get().claimed_id
800- self.assertEqual(claimed_id, settings.OPENID_SSO_SERVER_URL + '+id/' +
801- consumer.key)
802-
803- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
804- def test_stale_consumer(self, mock_get_data):
805- """Check that having a stale stored Consumer doesn't break auth"""
806- user = self.factory.makeUser()
807- # create a (stale) consumer
808- self.factory.makeOAuthTokenAndConsumer(user=user)
809- # and a new one
810- token, consumer = self.factory.makeOAuthTokenAndConsumer(user=user,
811- save=False)
812- request = MockRequest()
813- request.META.update(self.header_from_token(request, token, consumer))
814- mock_get_data.return_value = mock_data_for_account(consumer, token)
815+ openid = new_user.useropenid_set.get()
816+ expected_id = (settings.OPENID_SSO_SERVER_URL.strip('/') + '/+id/' +
817+ consumer.key)
818+ self.assertEqual(expected_id, openid.claimed_id)
819+ self.assertEqual(expected_id, openid.display_id)
820+
821+ def test_full_claimed_id(self):
822+ """Simple test to check that the right claimed_id is calculated"""
823+ consumer_key = 'abcdef12'
824+
825+ claimed_id = full_claimed_id(consumer_key)
826+
827+ expected_id = (settings.OPENID_SSO_SERVER_URL.strip('/') + '/+id/' +
828+ consumer_key)
829+ self.assertEqual(expected_id, claimed_id)
830+
831+ def test_query_string_auth(self):
832+ request, token, consumer = self.make_request_with_querystring_token()
833
834 response = self.get_resource(request)
835
836- # The original consumer has been replaced with the new one from SSO.
837- self.assertEqual(1, Consumer.objects.filter(key=consumer.key).count())
838- self.assertEqual(consumer.secret,
839- Consumer.objects.get(key=consumer.key).secret)
840-
841- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
842- def test_first_and_last_name_updated(self, mock_get_data):
843- """Check that the users name is updated from SSO."""
844- user = self.factory.makeUser(first_name='Harry', last_name='Jones')
845+ auth_data = {
846+ 'http_url': request.build_absolute_uri(),
847+ 'http_method': request.method,
848+ 'authorization': request.META.get('HTTP_AUTHORIZATION', None),
849+ 'query_string': request.META.get('QUERY_STRING', None),
850+ }
851+ self.mock_validate.assert_called_with(data=auth_data)
852+ self.assertEqual(httplib.OK, response.status_code)
853+
854+ @patch('reviewsapp.auth.oauth.OAuthRequest.from_request')
855+ def test_oauth_request_with_authorization_header(
856+ self, mock_oauth_from_request):
857+ request, token, consumer = self.make_request_with_querystring_token()
858+ request.META.update({'HTTP_AUTHORIZATION': 'someauth'})
859+
860+ ret = Mock()
861+ ret.get_parameter.side_effect = ['openid', 'token_key']
862+ mock_oauth_from_request.return_value = ret
863+
864+ self.get_resource(request)
865+
866+ auth_data = {
867+ 'http_url': request.build_absolute_uri(),
868+ 'http_method': request.method,
869+ 'headers': {'Authorization': 'someauth'},
870+ 'query_string': request.META.get('QUERY_STRING', None),
871+ }
872+
873+ mock_oauth_from_request.assert_called_with(**auth_data)
874+
875+ @patch('reviewsapp.auth.oauth.OAuthRequest.from_request')
876+ def test_oauth_request_with_empty_authorization_header(
877+ self, mock_oauth_from_request):
878+ request, token, consumer = self.make_request_with_querystring_token()
879+ request.META.update({'HTTP_AUTHORIZATION': ''})
880+
881+ ret = Mock()
882+ ret.get_parameter.side_effect = ['openid', 'token_key']
883+ mock_oauth_from_request.return_value = ret
884+
885+ self.get_resource(request)
886+
887+ auth_data = {
888+ 'http_url': request.build_absolute_uri(),
889+ 'http_method': request.method,
890+ 'headers': {},
891+ 'query_string': request.META.get('QUERY_STRING', None),
892+ }
893+
894+ mock_oauth_from_request.assert_called_with(**auth_data)
895+
896+ @patch('reviewsapp.auth.oauth.OAuthRequest.from_request')
897+ def test_oauth_request_sans_authorization_header(
898+ self, mock_oauth_from_request):
899+ request, token, consumer = self.make_request_with_querystring_token()
900+ request.META.update({'HTTP_AUTHORIZATION': None})
901+
902+ ret = Mock()
903+ ret.get_parameter.side_effect = ['openid', 'token_key']
904+ mock_oauth_from_request.return_value = ret
905+
906+ self.get_resource(request)
907+
908+ auth_data = {
909+ 'http_url': request.build_absolute_uri(),
910+ 'http_method': request.method,
911+ 'headers': {},
912+ 'query_string': request.META.get('QUERY_STRING', None),
913+ }
914+
915+ mock_oauth_from_request.assert_called_with(**auth_data)
916+
917+ def test_user_is_updated_from_sso_user_data_on_login(self):
918+ """Check that the data is updated from SSO."""
919+ user = self.factory.makeUser(
920+ first_name='Harry', last_name='Jones', email='old@email.com')
921 # create a (stale) consumer
922 self.factory.makeOAuthTokenAndConsumer(user=user)
923 # and a new one
924- token, consumer = self.factory.makeOAuthTokenAndConsumer(user=user,
925- save=False)
926+ token, consumer = self.factory.makeOAuthTokenAndConsumer(
927+ user=user, save=False)
928 request = MockRequest()
929- request.META.update(self.header_from_token(request, token, consumer))
930- mock_get_data.return_value = mock_data_for_account(
931- consumer, token, displayname='Hazel Jennet')
932+ request.META.update(self.factory.header_from_token(
933+ request, token, consumer))
934+ self.mock_get_data.return_value = self.mock_data_for_account(
935+ consumer, displayname='Hazel Jennet', email='new@email.com')
936
937 response = self.get_resource(request)
938
939 # The original first and last names have been updated from SSO.
940- user = User.objects.get(id=user.id)
941+ user = User.objects.get(pk=user.pk)
942 self.assertEqual('Hazel Jennet', user.get_full_name())
943-
944- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
945- def test_auth_succeedes_when_more_than_one_consumer(self, mock_get_data):
946- """
947- Check that auth works even when there are two consumers for one token
948-
949- Bug: #706035
950-
951- """
952- user1 = self.factory.makeUser()
953- user2 = self.factory.makeUser()
954-
955- token1, consumer1 = self.factory.makeOAuthTokenAndConsumer(user=user1)
956- token2, consumer2 = self.factory.makeOAuthTokenAndConsumer(user=user2)
957- consumer2.key = consumer1.key
958- consumer2.save()
959-
960+ self.assertEqual('new@email.com', user.email)
961+ self.assertEqual(httplib.OK, response.status_code)
962+
963+ def test_existing_user_authenticates_even_with_missing_sso_data(self):
964+ request, token, consumer = self.make_request_with_querystring_token()
965+ consumer.user.delete()
966+ self.mock_get_data.return_value = None
967+ response = self.get_resource(request)
968+ self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
969+
970+ user = self.factory.makeUser(
971+ first_name='Harry', last_name='Jones', email='old@email.com')
972+ # create a (stale) consumer
973+ self.factory.makeOAuthTokenAndConsumer(user=user)
974+ # and a new one
975+ token, consumer = self.factory.makeOAuthTokenAndConsumer(
976+ user=user, save=False)
977 request = MockRequest()
978- request.META.update(
979- self.header_from_token(request, token2, consumer2))
980- mock_get_data.return_value = mock_data_for_account(consumer2, token2)
981-
982- response = self.get_resource(request)
983- self.assertEquals(response.status_code, 200)
984-
985- def make_request_with_token(self, save=True, method='GET'):
986- token, consumer = self.factory.makeOAuthTokenAndConsumer(save=save)
987- request = MockRequest(method=method)
988- request.META.update(self.header_from_token(request, token, consumer))
989- return request, token, consumer
990+ request.META.update(self.factory.header_from_token(
991+ request, token, consumer))
992+ self.mock_get_data.return_value = None
993+
994+ response = self.get_resource(request)
995+
996+ user = User.objects.get(pk=user.pk)
997+ self.assertEqual('Harry Jones', user.get_full_name())
998+ self.assertEqual('old@email.com', user.email)
999+ self.assertEqual(httplib.OK, response.status_code)
1000+
1001+ def test_invalid_user_and_no_sso_data_is_unauthorized(self):
1002+ request, token, consumer = self.make_request_with_querystring_token()
1003+ consumer.user.delete()
1004+ self.mock_get_data.return_value = None
1005+ response = self.get_resource(request)
1006+ self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
1007+
1008+
1009+class UpdateOrCreateUserFromOauthTestCase(BaseAuthTestCase):
1010+
1011+ def setUp(self):
1012+ super(UpdateOrCreateUserFromOauthTestCase, self).setUp()
1013+ p = patch('reviewsapp.auth.web_services')
1014+ self.mock_web_services = p.start()
1015+ self.addCleanup(p.stop)
1016+
1017+ self.mock_get_data = self.mock_web_services.get_data_for_account
1018+ self.mock_get_data.return_value = self.mock_data_for_account()
1019+
1020+ web_services.refresh_connections()
1021+
1022+ def assert_user_data(self, user, sso_user_data):
1023+ first_name, _, last_name = sso_user_data['displayname'].rpartition(' ')
1024+ email = sso_user_data['email']
1025+ self.assertEqual(user.first_name, first_name)
1026+ self.assertEqual(user.last_name, last_name)
1027+ self.assertEqual(user.email, email)
1028+
1029+ def test_missing_account_data_and_missing_user(self):
1030+ self.mock_get_data.return_value = None
1031+ user, created = update_or_create_user_from_oauth('inexistent')
1032+ self.assertIsNone(user)
1033+ self.assertFalse(created)
1034+
1035+ def test_account_data_with_no_email_and_missing_user(self):
1036+ sso_user_data = self.mock_data_for_account(email='')
1037+ self.mock_get_data.return_value = sso_user_data
1038+ user, created = update_or_create_user_from_oauth('inexistent')
1039+ self.assertIsNone(user)
1040+ self.assertFalse(created)
1041+
1042+ def test_account_data_with_no_displayname_and_missing_user(self):
1043+ sso_user_data = self.mock_data_for_account(displayname=None)
1044+ self.mock_get_data.return_value = sso_user_data
1045+ user, created = update_or_create_user_from_oauth('inexistent')
1046+ self.assertIsNone(user)
1047+ self.assertFalse(created)
1048+
1049+ def test_account_data_unverified_and_missing_user(self):
1050+ sso_user_data = self.mock_data_for_account(verified=False)
1051+ self.mock_get_data.return_value = sso_user_data
1052+ user, created = update_or_create_user_from_oauth('inexistent')
1053+ self.assertIsNone(user)
1054+ self.assertFalse(created)
1055+
1056+ def test_account_data_and_missing_user(self):
1057+ sso_user_data = self.mock_data_for_account()
1058+ self.mock_get_data.return_value = sso_user_data
1059+ user, created = update_or_create_user_from_oauth('inexistent')
1060+ self.assertIsNotNone(user)
1061+ self.assertTrue(created)
1062+ self.assert_user_data(user, sso_user_data)
1063+
1064+ def test_account_data_and_existing_user(self):
1065+ sso_user_data = self.mock_data_for_account(
1066+ displayname='John Does', email='user@email.com')
1067+ self.mock_get_data.return_value = sso_user_data
1068+ openid = full_claimed_id('existent')
1069+ self.factory.makeUser(
1070+ first_name='John', last_name='Does', email='user@email.com',
1071+ open_id=openid)
1072+ user, created = update_or_create_user_from_oauth('existent')
1073+ self.assertIsNotNone(user)
1074+ self.assertFalse(created)
1075+ self.assert_user_data(user, sso_user_data)
1076+
1077+ def test_missing_account_data_and_existing_user(self):
1078+ self.mock_get_data.return_value = None
1079+ openid = full_claimed_id('existent')
1080+ self.factory.makeUser(
1081+ first_name='John', last_name='Does', email='user@email.com',
1082+ open_id=openid)
1083+ user, created = update_or_create_user_from_oauth('existent')
1084+ self.assertIsNotNone(user)
1085+ self.assertFalse(created)
1086+
1087+ def test_account_data_updates_user_data(self):
1088+ sso_user_data = self.mock_data_for_account(
1089+ displayname='Juan Hace', email='juan@email.com')
1090+ self.mock_get_data.return_value = sso_user_data
1091+ openid = full_claimed_id('existent')
1092+ self.factory.makeUser(
1093+ first_name='John', last_name='Does', email='user@email.com',
1094+ open_id=openid)
1095+ user, created = update_or_create_user_from_oauth('existent')
1096+ self.assertIsNotNone(user)
1097+ self.assertFalse(created)
1098+ self.assert_user_data(user, sso_user_data)
1099+
1100+ def test_unverified_account_data_doesnt_updates_user_data(self):
1101+ sso_user_data = self.mock_data_for_account(
1102+ verified=False, displayname='Juan Hace', email='juan@email.com')
1103+ self.mock_get_data.return_value = sso_user_data
1104+ openid = full_claimed_id('existent')
1105+ self.factory.makeUser(
1106+ first_name='John', last_name='Does', email='user@email.com',
1107+ open_id=openid)
1108+ user, created = update_or_create_user_from_oauth('existent')
1109+ self.assertIsNotNone(user)
1110+ self.assertFalse(created)
1111+ self.assertEqual(user.get_full_name(), 'John Does')
1112+ self.assertEqual(user.email, 'user@email.com')
1113+
1114+ def test_account_data_with_missing_email_doesnt_updates_user_data(self):
1115+ sso_user_data = self.mock_data_for_account(
1116+ displayname='Juan Hace', email='')
1117+ self.mock_get_data.return_value = sso_user_data
1118+ openid = full_claimed_id('existent')
1119+ self.factory.makeUser(
1120+ first_name='John', last_name='Does', email='user@email.com',
1121+ open_id=openid)
1122+ user, created = update_or_create_user_from_oauth('existent')
1123+ self.assertIsNotNone(user)
1124+ self.assertFalse(created)
1125+ self.assertEqual(user.get_full_name(), 'John Does')
1126+ self.assertEqual(user.email, 'user@email.com')
1127+
1128+ def test_account_data_with_missing_name_doesnt_updates_user_data(self):
1129+ sso_user_data = self.mock_data_for_account(
1130+ displayname=None, email='new@email.com')
1131+ self.mock_get_data.return_value = sso_user_data
1132+ openid = full_claimed_id('existent')
1133+ self.factory.makeUser(
1134+ first_name='John', last_name='Does', email='user@email.com',
1135+ open_id=openid)
1136+ user, created = update_or_create_user_from_oauth('existent')
1137+ self.assertIsNotNone(user)
1138+ self.assertFalse(created)
1139+ self.assertEqual(user.get_full_name(), 'John Does')
1140+ self.assertEqual(user.email, 'user@email.com')
1141
1142=== modified file 'src/reviewsapp/tests/test_rnrclient.py'
1143--- src/reviewsapp/tests/test_rnrclient.py 2014-07-03 17:57:21 +0000
1144+++ src/reviewsapp/tests/test_rnrclient.py 2014-07-18 11:18:53 +0000
1145@@ -37,12 +37,13 @@
1146 from datetime import timedelta
1147
1148 from decimal import Decimal
1149+from django.conf import settings
1150+from django.contrib.auth.models import User
1151+from django.core.cache import cache
1152 from django.core.handlers.wsgi import WSGIHandler
1153-from django.core.cache import cache
1154-from mock import (
1155- Mock,
1156- patch,
1157-)
1158+from django_openid_auth.models import UserOpenID
1159+from mock import patch
1160+
1161 from piston_mini_client.validators import ValidationException
1162 from piston_mini_client.auth import OAuthAuthorizer
1163 from rnrclient import RatingsAndReviewsAPI, ReviewRequest
1164@@ -56,7 +57,6 @@
1165 from reviewsapp.tests.helpers import (
1166 patch_settings,
1167 WSGIInterceptedTestCase,
1168- mock_data_for_account,
1169 )
1170 from reviewsapp.tests.factory import (
1171 TestCaseWithFactory,
1172@@ -65,19 +65,64 @@
1173 from reviewsapp.utilities import invalidate_paginated_reviews_for
1174
1175
1176-class RnRBaseTestCase(WSGIInterceptedTestCase, TestCaseWithFactory):
1177- def setUp(self):
1178- callbacks = {('localhost', 8000): WSGIHandler}
1179- super(RnRBaseTestCase, self).setUp(callbacks)
1180-
1181-
1182-class RnRTxBaseTestCase(WSGIInterceptedTestCase, TxTestCaseWithFactory):
1183- def setUp(self):
1184- callbacks = {('localhost', 8000): WSGIHandler}
1185- super(RnRTxBaseTestCase, self).setUp(callbacks)
1186-
1187-
1188-class RnRServerStatusTestCase(RnRBaseTestCase):
1189+class RnRClientTestCaseBase(WSGIInterceptedTestCase):
1190+
1191+ def setUp(self):
1192+ callbacks = {('localhost', 8000): WSGIHandler}
1193+ super(RnRClientTestCaseBase, self).setUp(callbacks)
1194+
1195+ def make_api_for_user(
1196+ self, user=None, mock_get_data_returns=None, mock_verify=None):
1197+ if user is None:
1198+ user = self.factory.makeUser()
1199+
1200+ consumer_id = 'consumer_key'
1201+ email = 'foo@bar.com'
1202+
1203+ if user is not None and not user.is_anonymous():
1204+ # Set correct consumer id and email in case of valid user
1205+ try:
1206+ claimed_id = user.useropenid_set.latest('pk').claimed_id
1207+ consumer_id = claimed_id.rsplit('/', 1)[1]
1208+ except (UserOpenID.DoesNotExist, IndexError):
1209+ pass
1210+ email = user.email
1211+
1212+ auth = OAuthAuthorizer(
1213+ 'token', 'secret', consumer_id, 'consumer_secret')
1214+
1215+ patcher = patch('reviewsapp.auth.web_services')
1216+ mock_ws = patcher.start()
1217+ self.addCleanup(patcher.stop)
1218+
1219+ if mock_get_data_returns is None:
1220+ mock_get_data_returns = {
1221+ 'displayname': 'Some Body',
1222+ 'email': email,
1223+ 'verified': True,
1224+ }
1225+ # SSO will validate the request
1226+ mock_ws.validate_request.return_value = True
1227+ mock_ws.get_data_for_account.return_value = mock_get_data_returns
1228+
1229+ if mock_verify is None:
1230+ patcher = patch('reviewsapp.forms.web_services')
1231+ mock_ws = patcher.start()
1232+ self.addCleanup(patcher.stop)
1233+ mock_ws.verify_packagename_in_repository.return_value = True
1234+
1235+ return RatingsAndReviewsAPI(auth=auth)
1236+
1237+
1238+class RnRClientTestCase(RnRClientTestCaseBase, TestCaseWithFactory):
1239+ pass
1240+
1241+
1242+class RnRClientTxTestCase(RnRClientTestCaseBase, TxTestCaseWithFactory):
1243+ pass
1244+
1245+
1246+class RnRServerStatusTestCase(RnRClientTestCase):
1247 def test_server_status(self):
1248 api = RatingsAndReviewsAPI()
1249
1250@@ -86,7 +131,7 @@
1251 self.assertEqual('ok', response)
1252
1253
1254-class RnRReviewStatsTestCase(RnRTxBaseTestCase):
1255+class RnRReviewStatsTestCase(RnRClientTxTestCase):
1256
1257 def clear_cache(self):
1258 cache.clear()
1259@@ -181,7 +226,7 @@
1260 self.assertEqual(None, response.get('content-encoding', None))
1261
1262
1263-class RnRGetReviewsTestCase(RnRBaseTestCase):
1264+class RnRGetReviewsTestCase(RnRClientTestCase):
1265 def test_get_reviews_for_package(self):
1266 review = self.factory.makeReview()
1267 api = RatingsAndReviewsAPI()
1268@@ -264,7 +309,7 @@
1269 self.assertEqual(response['vary'], 'Accept')
1270
1271
1272-class RnRGetReviewTestCase(RnRBaseTestCase):
1273+class RnRGetReviewTestCase(RnRClientTestCase):
1274
1275 def test_get_review(self):
1276 review = self.factory.makeReview()
1277@@ -278,59 +323,31 @@
1278 invalidate_paginated_reviews_for(review)
1279
1280
1281-class RnRDeleteReviewTestCase(RnRTxBaseTestCase):
1282+class RnRDeleteReviewTestCase(RnRClientTxTestCase):
1283
1284 def test_delete_review(self):
1285- user = self.factory.makeUser()
1286- review = self.factory.makeReview(reviewer=user)
1287- token, consumer = self.factory.makeOAuthTokenAndConsumer(user=user)
1288-
1289- auth = OAuthAuthorizer(
1290- token.token, token.token_secret, consumer.key, consumer.secret)
1291-
1292- lp_verify_packagename_method = (
1293- 'reviewsapp.utilities.WebServices.'
1294- 'verify_packagename_in_repository')
1295-
1296- mock_verify_package = Mock()
1297- mock_verify_package.return_value = True
1298-
1299- api = RatingsAndReviewsAPI(auth=auth)
1300- with patch(lp_verify_packagename_method, mock_verify_package):
1301- response = api.delete_review(review_id=int(review.id))
1302+ review = self.factory.makeReview()
1303+ api = self.make_api_for_user(review.reviewer)
1304+ response = api.delete_review(review_id=review.pk)
1305
1306 self.assertEqual(review.id, response['id'])
1307 self.assertNotEqual(None, response['date_deleted'])
1308
1309
1310-class RnRModifyReviewTestCase(RnRTxBaseTestCase):
1311+class RnRModifyReviewTestCase(RnRClientTxTestCase):
1312
1313 def test_modify_review(self):
1314- user = self.factory.makeUser()
1315- review = self.factory.makeReview(reviewer=user)
1316- token, consumer = self.factory.makeOAuthTokenAndConsumer(user=user)
1317-
1318- auth = OAuthAuthorizer(
1319- token.token, token.token_secret, consumer.key, consumer.secret)
1320-
1321- lp_verify_packagename_method = (
1322- 'reviewsapp.utilities.WebServices.'
1323- 'verify_packagename_in_repository')
1324-
1325- mock_verify_package = Mock()
1326- mock_verify_package.return_value = True
1327-
1328- api = RatingsAndReviewsAPI(auth=auth)
1329+ review = self.factory.makeReview()
1330+ api = self.make_api_for_user(review.reviewer)
1331
1332 rating = 4
1333 summary = 'summary'
1334 review_text = 'review text'
1335
1336- with patch(lp_verify_packagename_method, mock_verify_package):
1337- response = api.modify_review(review_id=int(review.id),
1338- rating=rating,
1339- summary=summary,
1340- review_text=review_text)
1341+ response = api.modify_review(review_id=int(review.id),
1342+ rating=rating,
1343+ summary=summary,
1344+ review_text=review_text)
1345
1346 self.assertEqual(review.id, response.id)
1347 self.assertEqual(rating, response.rating)
1348@@ -338,33 +355,12 @@
1349 self.assertEqual(review_text, response.review_text)
1350
1351
1352-class RnRSubmitReviewTestCase(RnRTxBaseTestCase):
1353+class RnRSubmitReviewTestCase(RnRClientTxTestCase):
1354
1355- def _submit_review(
1356- self, review_request, token_and_consumer=None,
1357- mock_verify_package=None):
1358+ def _submit_review(self, review_request, user=None, mock_verify=None):
1359 """Submit a review request and return the result with a mock."""
1360- if token_and_consumer is None:
1361- token, consumer = self.factory.makeOAuthTokenAndConsumer(
1362- user=self.factory.makeUser())
1363- else:
1364- token, consumer = token_and_consumer
1365-
1366- auth = OAuthAuthorizer(
1367- token.token, token.token_secret, consumer.key, consumer.secret)
1368- lp_verify_packagename_method = (
1369- 'reviewsapp.utilities.WebServices.'
1370- 'verify_packagename_in_repository')
1371-
1372- if mock_verify_package is None:
1373- mock_verify_package = Mock()
1374- mock_verify_package.return_value = True
1375-
1376- api = RatingsAndReviewsAPI(auth=auth)
1377- with patch(lp_verify_packagename_method, mock_verify_package):
1378- response = api.submit_review(review=review_request)
1379-
1380- return response, mock_verify_package
1381+ api = self.make_api_for_user(user, mock_verify=mock_verify)
1382+ return api.submit_review(review=review_request)
1383
1384 def _make_review_request(self, **kwargs):
1385 review_options = dict(
1386@@ -385,7 +381,7 @@
1387 package_name=package_name, rating=4,
1388 review_text=u'This is great.')
1389
1390- review_result, ignored = self._submit_review(review)
1391+ review_result = self._submit_review(review)
1392
1393 self.assertEqual(package_name, review_result.package_name)
1394 self.assertEqual(4, review_result.rating)
1395@@ -396,88 +392,78 @@
1396 api = RatingsAndReviewsAPI()
1397 self.assertRaises(ValidationException, api.submit_review)
1398
1399- @patch('reviewsapp.utilities.WebServices.get_data_for_account')
1400- def test_submit_review_fetch(self, mock_get_data):
1401+ def test_submit_review_fetch(self):
1402 """Submit a review using a token/consumer that we don't have.
1403
1404 The auth layer will need to fetch the information from SSO.
1405 """
1406- # Create a token/consumer pair without saving locally, and ensure that
1407- # it is returned by the get_data_for_account.
1408- token, consumer = self.factory.makeOAuthTokenAndConsumer(
1409- user=self.factory.makeUser(), save=False)
1410- mock_get_data.return_value = mock_data_for_account(consumer, token)
1411+ assert User.objects.count() == 0
1412 review = self._make_review_request()
1413
1414- review_result, ignored = self._submit_review(
1415- review, (token, consumer))
1416+ # XXX: This hack is avoid automatic user creation by
1417+ # `self.make_api_for_user `.
1418+ from django.contrib.auth.models import AnonymousUser
1419+ review_result = self._submit_review(review, AnonymousUser())
1420
1421 self.assertEqual('pacman', review_result.package_name)
1422- self.assertEqual(1, mock_get_data.call_count)
1423+ self.assertEqual(1, User.objects.count())
1424
1425- def test_submit_review_nonexisting_distroseries(self):
1426- review = self._make_review_request(
1427- package_name='pacman', distroseries='lucky',
1428- review_text='First review')
1429+ @patch.multiple(settings, ALLOW_MULTIPLE_REVIEWS_FOR_TESTING=True)
1430+ @patch('reviewsapp.forms.web_services.verify_packagename_in_repository')
1431+ def test_submit_review_nonexisting_distroseries(self, mock_verify):
1432+ mock_verify.return_value = True
1433
1434 # First time for this package_name & distroseries - should call verify
1435- mock_verify_package = Mock()
1436- response, mock_verify_package = self._submit_review(
1437- review, mock_verify_package=mock_verify_package)
1438-
1439- self.assertEqual(mock_verify_package.call_count, 1)
1440+ review = self._make_review_request(
1441+ package_name='pacman', distroseries='lucky',
1442+ review_text='First review')
1443+ self._submit_review(review, mock_verify=mock_verify)
1444+ self.assertEqual(mock_verify.call_count, 1)
1445
1446 # Same package_name & distroseries - should not call verify
1447 review = self._make_review_request(
1448 package_name='pacman', distroseries='lucky',
1449 review_text='Second review')
1450- response, mock_verify_package = self._submit_review(
1451- review, mock_verify_package=mock_verify_package)
1452+ self._submit_review(review, mock_verify=mock_verify)
1453
1454- self.assertEqual(mock_verify_package.call_count, 1)
1455+ self.assertEqual(mock_verify.call_count, 1)
1456
1457 # Change the distroseries - should call verify
1458 review = self._make_review_request(
1459 package_name='pacman', distroseries='bogus',
1460 review_text='Third review')
1461- response, mock_verify_package = self._submit_review(
1462- review, mock_verify_package=mock_verify_package)
1463- self.assertEqual(mock_verify_package.call_count, 2)
1464-
1465- def test_submit_review_different_archs(self):
1466- review = self._make_review_request(
1467- package_name='pacman', arch_tag='i386',
1468- review_text='First review')
1469-
1470+ self._submit_review(review, mock_verify=mock_verify)
1471+ self.assertEqual(mock_verify.call_count, 2)
1472+
1473+ @patch.multiple(settings, ALLOW_MULTIPLE_REVIEWS_FOR_TESTING=True)
1474+ @patch('reviewsapp.forms.web_services.verify_packagename_in_repository')
1475+ def test_submit_review_different_archs(self, mock_verify):
1476 # First time for this package_name & arch - should call verify
1477- mock_verify_package = Mock()
1478- response, mock_verify_package = self._submit_review(
1479- review, mock_verify_package=mock_verify_package)
1480-
1481- self.assertEqual(mock_verify_package.call_count, 1)
1482+ review = self._make_review_request(
1483+ package_name='pacman', arch_tag='i386',
1484+ review_text='First review')
1485+ self._submit_review(review, mock_verify=mock_verify)
1486+ self.assertEqual(mock_verify.call_count, 1)
1487
1488 # Same package_name & arch - should not call verify
1489 review = self._make_review_request(
1490 package_name='pacman', arch_tag='i386',
1491 review_text='Second review')
1492- response, mock_verify_package = self._submit_review(
1493- review, mock_verify_package=mock_verify_package)
1494-
1495- self.assertEqual(mock_verify_package.call_count, 1)
1496+ self._submit_review(review, mock_verify=mock_verify)
1497+ self.assertEqual(mock_verify.call_count, 1)
1498
1499 # Change the arch - should call verify
1500 review = self._make_review_request(
1501 package_name='pacman', arch_tag='amd64',
1502 review_text='Third review')
1503- response, mock_verify_package = self._submit_review(
1504- review, mock_verify_package=mock_verify_package)
1505- self.assertEqual(mock_verify_package.call_count, 2)
1506+ self._submit_review(review, mock_verify=mock_verify)
1507+ self.assertEqual(mock_verify.call_count, 2)
1508
1509 def test_passive_mode(self):
1510 # The server is in passive mode by default
1511 review = self._make_review_request()
1512
1513- review_result, ignored = self._submit_review(review)
1514+ review_result = self._submit_review(review)
1515 # In passive mode the review will not be hidden
1516 self.assertFalse(review_result.hide)
1517
1518@@ -491,7 +477,7 @@
1519
1520 review = self._make_review_request()
1521
1522- review_result, ignored = self._submit_review(review)
1523+ review_result = self._submit_review(review)
1524 # In active mode the review will be hidden
1525 self.assertTrue(review_result.hide)
1526
1527@@ -499,44 +485,29 @@
1528 self.assertEquals(ReviewModeration.objects.count(), 1)
1529 self.assertEquals(ReviewModerationFlag.objects.count(), 1)
1530 moderation = ReviewModeration.objects.all()[0]
1531- flag = ReviewModerationFlag.objects.all()[0]
1532+ ReviewModerationFlag.objects.all()[0]
1533 # ensure that the review is in pending status
1534 self.assertEqual(moderation.status, ReviewModeration.PENDING_STATUS)
1535
1536
1537-class RnRFlagReviewTestCase(RnRBaseTestCase):
1538-
1539- def _flag_review(self, review_id, reason, text):
1540- token, consumer = self.factory.makeOAuthTokenAndConsumer(
1541- user=self.factory.makeUser())
1542-
1543- auth = OAuthAuthorizer(
1544- token.token, token.token_secret, consumer.key, consumer.secret)
1545-
1546- api = RatingsAndReviewsAPI(auth=auth)
1547- return api.flag_review(
1548- review_id=int(review_id), reason=reason, text=text)
1549+class RnRFlagReviewTestCase(RnRClientTestCase):
1550
1551 def test_flag_review_returns_new_flag(self):
1552- # The newly created flag is returned when calling api.flag_review.
1553- review = self.factory.makeReview()
1554-
1555- flag = self._flag_review(review.id, 'my_reason', 'my text')
1556-
1557+ user = self.factory.makeUser()
1558+ api = self.make_api_for_user(user)
1559+ review = self.factory.makeReview(reviewer=user)
1560+ flag = api.flag_review(
1561+ review_id=int(review.pk), reason='my_reason', text='my text')
1562 self.assertEqual('my_reason', flag['summary'])
1563
1564
1565-class UsefulnessAPITestCase(RnRTxBaseTestCase):
1566+class UsefulnessAPITestCase(RnRClientTxTestCase):
1567 def setUp(self):
1568 super(UsefulnessAPITestCase, self).setUp()
1569 self.user = self.factory.makeUser()
1570
1571 def test_submit_useful(self):
1572- token, consumer = self.factory.makeOAuthTokenAndConsumer(
1573- user=self.user)
1574- auth = OAuthAuthorizer(
1575- token.token, token.token_secret, consumer.key, consumer.secret)
1576- api = RatingsAndReviewsAPI(auth=auth)
1577+ api = self.make_api_for_user(self.user)
1578 software_item = self.factory.makeSoftwareItem()
1579 review = self.factory.makeReview(software_item=software_item)
1580 response = api.submit_usefulness(
1581@@ -546,11 +517,7 @@
1582 self.assertEqual(1, count)
1583
1584 def test_submit_not_useful(self):
1585- token, consumer = self.factory.makeOAuthTokenAndConsumer(
1586- user=self.user)
1587- auth = OAuthAuthorizer(
1588- token.token, token.token_secret, consumer.key, consumer.secret)
1589- api = RatingsAndReviewsAPI(auth=auth)
1590+ api = self.make_api_for_user(self.user)
1591 software_item = self.factory.makeSoftwareItem()
1592 review = self.factory.makeReview(software_item=software_item)
1593 response = api.submit_usefulness(
1594@@ -560,11 +527,7 @@
1595 self.assertEqual(0, count)
1596
1597 def test_submit_useful_multiple_times(self):
1598- token, consumer = self.factory.makeOAuthTokenAndConsumer(
1599- user=self.user)
1600- auth = OAuthAuthorizer(
1601- token.token, token.token_secret, consumer.key, consumer.secret)
1602- api = RatingsAndReviewsAPI(auth=auth)
1603+ api = self.make_api_for_user(self.user)
1604 software_item = self.factory.makeSoftwareItem()
1605 review = self.factory.makeReview(software_item=software_item)
1606 response = api.submit_usefulness(
1607
1608=== modified file 'src/reviewsapp/tests/test_utilities.py'
1609--- src/reviewsapp/tests/test_utilities.py 2014-02-12 09:09:56 +0000
1610+++ src/reviewsapp/tests/test_utilities.py 2014-07-18 11:18:53 +0000
1611@@ -21,7 +21,7 @@
1612 __metaclass__ = type
1613 __all__ = [
1614 'FullClaimedIdTestCase',
1615- 'IdentityProviderTestCase',
1616+ 'SSOClientTestCase',
1617 'InvalidatePaginatedReviewsTestCase',
1618 'SCATestCase',
1619 'VerifyPackageTestCase',
1620@@ -33,13 +33,9 @@
1621 from django.conf import settings
1622 from django.core.cache import cache
1623 from django.core.urlresolvers import reverse
1624-from httplib2 import ServerNotFoundError
1625 from launchpadlib.errors import HTTPError
1626 from mock import patch, Mock
1627-from mockssoservice.mockserver import MockSSOServer, new_token
1628 from piston_mini_client import PistonResponseObject
1629-from wsgi_intercept import add_wsgi_intercept, remove_wsgi_intercept
1630-from wsgi_intercept.httplib2_intercept import install, uninstall
1631
1632 from reviewsapp.tests.helpers import patch_settings
1633 from reviewsapp.tests.factory import TestCaseWithFactory
1634@@ -47,59 +43,55 @@
1635 cache_key_for_url,
1636 invalidate_paginated_reviews_for,
1637 WebServices,
1638- WebServiceError,
1639 full_claimed_id,
1640+ web_services,
1641 )
1642
1643
1644-class IdentityProviderTestCase(TestCaseWithFactory):
1645- def mock_sso(self, *args):
1646- return MockSSOServer('login.ubuntu.com', 443, '/api/1.0',
1647- scheme='https')
1648+class SSOClientTestCase(TestCaseWithFactory):
1649
1650 def setUp(self):
1651- install()
1652- add_wsgi_intercept('login.ubuntu.com', 443, self.mock_sso)
1653- super(IdentityProviderTestCase, self).setUp()
1654-
1655- self.orig_sso_api_service_root = settings.SSO_API_SERVICE_ROOT
1656- self.orig_sso_api_auth_username = settings.SSO_API_AUTH_USERNAME
1657- self.orig_sso_api_auth_password = settings.SSO_API_AUTH_PASSWORD
1658-
1659- settings.SSO_API_SERVICE_ROOT = 'https://login.ubuntu.com/api/1.0'
1660- # Use the credentials for the mockserver
1661- settings.SSO_API_AUTH_USERNAME = 'MyUsername'
1662- settings.SSO_API_AUTH_PASSWORD = 'password'
1663-
1664- # Force our webservice client to recreate the the ServiceRoot
1665- WebServices._identity_provider = None
1666-
1667- def tearDown(self):
1668- super(IdentityProviderTestCase, self).tearDown()
1669- remove_wsgi_intercept('login.ubuntu.com', 443)
1670- uninstall()
1671- settings.SSO_API_SERVICE_ROOT = self.orig_sso_api_service_root
1672- settings.SSO_API_AUTH_USERNAME = self.orig_sso_api_auth_username
1673- settings.SSO_API_AUTH_PASSWORD = self.orig_sso_api_auth_password
1674-
1675- @patch('httplib2.Http.request')
1676- def test_identity_provider_unavailable(self, mock_request):
1677- mock_request.side_effect = ServerNotFoundError
1678- settings.SSO_API_SERVICE_ROOT = 'http://nothing.localhost/nonapi/3.3'
1679-
1680- self.assertRaises(
1681- WebServiceError, getattr, WebServices(), 'identity_provider')
1682-
1683- def test_get_data_for_account(self):
1684- # We tell the mockserver about the token we'll be checking.
1685- new_token('test-token', 'eg_token')
1686- web_services = WebServices()
1687- data = web_services.get_data_for_account('eg_token', 'eg_ident')
1688- fields = [
1689- u'username', u'preferred_email', u'displayname', u'name',
1690- u'unverified_emails', u'verified_emails', u'consumer_secret',
1691- u'token', u'openid_identifier', u'consumer_key', u'token_secret']
1692- self.assertEquals(set(fields), set(data))
1693+ super(SSOClientTestCase, self).setUp()
1694+
1695+ p = patch('reviewsapp.utilities.web_services._ssoclient')
1696+ self.mock_sso_client = p.start()
1697+ self.addCleanup(p.stop)
1698+
1699+ self.mock_validate_request = self.mock_sso_client.validate_request
1700+ self.mock_account_details = self.mock_sso_client.account_details
1701+ self.mock_token_details = self.mock_sso_client.token_details
1702+
1703+ def test_validate_request_ok(self):
1704+ self.mock_validate_request.return_value = dict(is_valid=True)
1705+ self.assertTrue(web_services.validate_request(data=dict()))
1706+
1707+ def test_validate_request_not_ok(self):
1708+ self.mock_validate_request.return_value = dict(is_valid=False)
1709+ self.assertFalse(web_services.validate_request(data=dict()))
1710+
1711+ def test_validate_request_failed(self):
1712+ self.mock_validate_request.side_effect = ValueError()
1713+ self.assertFalse(web_services.validate_request(data=dict()))
1714+
1715+ def test_validate_not_json(self):
1716+ self.mock_validate_request.return_value = 'this is not valid json'
1717+ self.assertFalse(web_services.validate_request(data=dict()))
1718+
1719+ def test_account_details(self):
1720+ self.mock_account_details.return_value = 'data'
1721+
1722+ response = web_services.get_data_for_account('open-id')
1723+ self.mock_account_details.assert_called_with(
1724+ 'open-id', token=settings.SSO_V2_API_TOKEN_DATA)
1725+ self.assertEqual(response, 'data')
1726+
1727+ def test_account_details_fail(self):
1728+ self.mock_account_details.side_effect = ValueError
1729+
1730+ response = web_services.get_data_for_account('open-id')
1731+ self.mock_account_details.assert_called_with(
1732+ 'open-id', token=settings.SSO_V2_API_TOKEN_DATA)
1733+ self.assertEqual(response, None)
1734
1735
1736 class VerifyPackageTestCase(TestCaseWithFactory):
1737@@ -403,7 +395,7 @@
1738 def test_invalidates_single_review_request(self):
1739 review = self.factory.makeReview()
1740 url = reverse('rnr-api-review-for-review-id', args=[review.id])
1741- response = self.client.get(url)
1742+ self.client.get(url)
1743 self.assertTrue(cache_key_for_url(url) in cache)
1744
1745 invalidate_paginated_reviews_for(review)
1746
1747=== modified file 'src/reviewsapp/utilities.py'
1748--- src/reviewsapp/utilities.py 2014-06-23 20:19:03 +0000
1749+++ src/reviewsapp/utilities.py 2014-07-18 11:18:53 +0000
1750@@ -28,12 +28,7 @@
1751
1752 import logging
1753 import math
1754-import urllib
1755-from httplib2 import (
1756- Http,
1757- HttpLib2Error,
1758- ServerNotFoundError,
1759-)
1760+from httplib2 import ServerNotFoundError
1761
1762 from django.conf import settings
1763 from django.core.cache import cache
1764@@ -44,12 +39,9 @@
1765
1766 from launchpadlib.launchpad import Launchpad
1767 from launchpadlib.uris import lookup_service_root
1768-from lazr.restfulclient.authorize import BasicHttpAuthorizer
1769-from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
1770 from lazr.restfulclient.errors import HTTPError
1771-from lazr.restfulclient.resource import ServiceRoot
1772-from oauth.oauth import OAuthToken
1773 from scaclient import SoftwareCenterAgentAPI
1774+from ssoclient.v2.client import V2ApiClient
1775
1776
1777 class WebServiceError(Exception):
1778@@ -57,39 +49,24 @@
1779
1780
1781 class WebServices:
1782- _identity_provider = None
1783 _launchpad_service = None
1784+ _ssoclient = None
1785
1786 def __init__(self):
1787 """Pre-create the identity provider webservice if it will be used."""
1788 if settings.PRELOAD_API_SERVICE_ROOTS:
1789- self._set_identity_provider()
1790+ self._set_ssoclient()
1791 self._set_launchpad_service()
1792
1793+ def refresh_connections(self):
1794+ """Ensure all http connections are dropped and restarted."""
1795+ self._launchpad_service = None
1796+ self._ssoclient = None
1797+
1798 @property
1799 def logger(self):
1800 return logging.getLogger('rnr.utilities')
1801
1802- def _create_service(self, authorizer, service_root_url):
1803- try:
1804- service_root = ServiceRoot(authorizer, service_root_url)
1805- self.logger.info(
1806- "Webservice root creation successful: %s" % service_root_url)
1807- return service_root
1808- except (HTTPError, ServerNotFoundError), e:
1809- self.logger.exception(
1810- "Failed to create service root: %s" % service_root_url)
1811- return None
1812-
1813- def _create_basic_auth_service(self, service_root_url, username, password):
1814- authorizer = BasicHttpAuthorizer(username, password)
1815- return self._create_service(authorizer, service_root_url)
1816-
1817- def _set_identity_provider(self):
1818- WebServices._identity_provider = self._create_basic_auth_service(
1819- settings.SSO_API_SERVICE_ROOT, settings.SSO_API_AUTH_USERNAME,
1820- settings.SSO_API_AUTH_PASSWORD)
1821-
1822 def _set_launchpad_service(self):
1823 service_root = lookup_service_root(settings.LP_SERVICE)
1824 launchpadlib_dir = settings.LAUNCHPADLIB_DIR
1825@@ -101,15 +78,8 @@
1826 self.logger.exception('Unable to create Launchpad API.')
1827 WebServices._launchpad_service = None
1828
1829- @property
1830- def identity_provider(self):
1831- if self._identity_provider is None:
1832- self._set_identity_provider()
1833-
1834- if self._identity_provider is None:
1835- raise WebServiceError("The payment service is not available.")
1836-
1837- return self._identity_provider
1838+ def _set_ssoclient(self):
1839+ self._ssoclient = V2ApiClient(settings.SSO_API_SERVICE_ROOT)
1840
1841 @property
1842 def launchpad_service(self):
1843@@ -121,55 +91,32 @@
1844
1845 return self._launchpad_service
1846
1847- def _fake_validate_token(self, token, openid_identifier, signature):
1848- """ This is a version of validate_token that gets the
1849- token_secret, consumer_secret from the plaintext signature.
1850-
1851- It will break once we use a real signature for oauth, but
1852- its very useful for testing as it does not require the special
1853- priviledges required for the call to
1854- identity_provider.authentications.validate_token()
1855-
1856- The token can still be validated (and will be) with a
1857- api.accounts.me() call
1858- """
1859- (consumer_secret, token_secret) = urllib.unquote(signature).split("&")
1860- result = {}
1861- result["token"] = token
1862- result["token_secret"] = token_secret
1863- result["consumer_key"] = openid_identifier
1864- result["consumer_secret"] = consumer_secret
1865- result["name"] = "fake-token"
1866- return result
1867-
1868- def get_data_for_account(self, token, openid_identifier, signature=None):
1869- self.logger.debug(
1870- "Getting data for account %s", openid_identifier)
1871- if settings.SSO_AUTH_MODE_NO_UBUNTU_SSO_PLAINTEXT_ONLY:
1872- # token extraction based on PLAINTEXT token, the function
1873- # "validate_token" is really a "get_secrets_for_token()" call
1874- result = self._fake_validate_token(
1875- token, openid_identifier, signature)
1876- else:
1877- result = self.identity_provider.authentications.validate_token(
1878- token=token, consumer_key=openid_identifier)
1879-
1880- if not result:
1881- self.logger.debug(
1882- "SSO did not validate token %s for account %s", token,
1883- openid_identifier)
1884- else:
1885- oauth_token = OAuthToken(result['token'], result['token_secret'])
1886- authorizer = OAuthAuthorizer(
1887- result['consumer_key'], result['consumer_secret'], oauth_token)
1888- api = self._create_service(authorizer,
1889- settings.SSO_API_SERVICE_ROOT)
1890- result.update(api.accounts.me())
1891-
1892- if not result:
1893- self.logger.debug(
1894- "SSO did not return data for account %s.", openid_identifier)
1895- return result
1896+ @property
1897+ def ssoclient(self):
1898+ if self._ssoclient is None:
1899+ self._set_ssoclient()
1900+ return self._ssoclient
1901+
1902+ def validate_request(self, data):
1903+ try:
1904+ response = self.ssoclient.validate_request(data=data)
1905+ result = response.get('is_valid', False)
1906+ except (AttributeError, TypeError, KeyError, ValueError):
1907+ result = False
1908+ return result
1909+
1910+ def get_data_for_account(self, openid_identifier):
1911+ """Return SSO account details for the given openid_identifier."""
1912+ data = None
1913+
1914+ try:
1915+ token = settings.SSO_V2_API_TOKEN_DATA
1916+ data = self.ssoclient.account_details(
1917+ openid_identifier, token=token)
1918+ except:
1919+ self.logger.exception('Cannot retrieve user data.')
1920+
1921+ return data
1922
1923 def verify_packagename_in_repository(
1924 self, pkgname, origin, distroseries, arch_tag):
1925@@ -261,6 +208,9 @@
1926 return pubs
1927
1928
1929+web_services = WebServices()
1930+
1931+
1932 def cache_key_for_url(url):
1933 """
1934 Return the key used by django's caching system for a (non-varied) url.

Subscribers

People subscribed via source and target branches