Merge lp:~canonical-isd-hackers/canonical-identity-provider/email-whitelist into lp:canonical-identity-provider/release

Proposed by Michael Foord
Status: Merged
Approved by: Michael Nelson
Approved revision: no longer in the source branch.
Merged at revision: 194
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/email-whitelist
Merge into: lp:canonical-identity-provider/release
Diff against target: 498 lines (+170/-48)
12 files modified
identityprovider/api10/forms.py (+2/-2)
identityprovider/models/captcha.py (+14/-4)
identityprovider/schema.py (+3/-0)
identityprovider/tests/test_auth.py (+5/-6)
identityprovider/tests/test_captcha.py (+2/-2)
identityprovider/tests/test_command_cleanup.py (+2/-2)
identityprovider/tests/test_forms.py (+18/-0)
identityprovider/tests/test_models_captcha.py (+67/-26)
identityprovider/tests/test_views_ui.py (+53/-3)
identityprovider/views/ui.py (+2/-1)
requirements.txt (+1/-1)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/email-whitelist
Reviewer Review Type Date Requested Status
Michael Nelson (community) code Approve
Review via email: mp+69158@code.launchpad.net

Commit message

Email addresses can be whitelisted to bypass captcha verification. Whitelist emails by providing a list of regular expressions to match emails in the EMAIL_WHITELIST_REGEXP_LIST setting (captcha section).

Description of the change

Email addresses can be whitelisted to bypass captcha verification. Whitelist emails by providing a list of regular expressions to match emails in the EMAIL_WHITELIST_REGEXP_LIST setting (captcha section).

This mp adds an EMAIL_WHITELIST_REGEXP_LIST setting to the captcha section of the sso config.

captcha.verify now takes an email address and all patterns will be checked against the supplied email. If a match is found then captcha verification is bypassed.

For our test email(s) I suggest the following pattern:

    ^canonicaltest(?:\+.+)?@gmail\.com$

So in local.cfg add:

[captcha]
email_whitelist_regexp_list =
    ^canonicaltest(?:\+.+)?@gmail\.com$

All tests pass in this branch.

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (11.1 KiB)

Hi Michael. Excellent branch... the implementation and tests are all very clear.

The only potential danger I can see with this branch is the ability to add unsafe regexes, but I don't see anyway around that unless we restricted ourselves to gmail accounts (ie. usesd settings.GMAIL_IDENTIFIER_WHITELIST and automatically calculated the regex to allow identifier+whatever).

The only questions I've got below are for my own understanding or tiny style questions. I verified that all 611 tests pass.

> === modified file 'identityprovider/models/captcha.py'
> --- identityprovider/models/captcha.py 2011-06-30 04:55:13 +0000
> +++ identityprovider/models/captcha.py 2011-07-26 07:31:31 +0000
> @@ -190,3 +194,9 @@
> self._verified = False
> raise VerifyCaptchaError(self.response.traceback)
> return self._verified
> +
> + def check_whitelist(self, email):
> + for pattern in settings.EMAIL_WHITELIST_REGEXP_LIST:
> + if re.match(pattern, email):
> + return True
> + return False

So that's the crux of the branch right? Nice and simple :)

The only thought I had was whether it should be *outside* of captcha.verify(),
rather than being hidden within, but that's just a small point - nothing to
delay the branch landing over :)

>
> === modified file 'identityprovider/schema.py'
> --- identityprovider/schema.py 2011-07-12 09:41:08 +0000
> +++ identityprovider/schema.py 2011-07-26 07:31:31 +0000
> @@ -78,6 +78,9 @@
> captcha.captcha_public_key = StringConfigOption()
> captcha.captcha_private_key = StringConfigOption()
> captcha.disable_captcha_verification = BoolConfigOption()
> + captcha.email_whitelist_regexp_list = LinesConfigOption(
> + item=StringConfigOption(raw=True)

I wasn't sure at first why the raw=True was necessary, until I tried
repr('^canonicaltest(?:\+.+)?@gmail\.com$').

> + )
>
> # debug
> debug = ConfigSection()
>
> === modified file 'identityprovider/tests/test_auth.py'
> --- identityprovider/tests/test_auth.py 2011-07-15 15:17:19 +0000
> +++ identityprovider/tests/test_auth.py 2011-07-26 07:31:31 +0000

Great to fix up all the patch_object's :)

> === modified file 'identityprovider/tests/test_captcha.py'
> --- identityprovider/tests/test_captcha.py 2011-07-21 16:08:06 +0000
> +++ identityprovider/tests/test_captcha.py 2011-07-26 07:31:31 +0000
> === modified file 'identityprovider/tests/test_command_cleanup.py'
> --- identityprovider/tests/test_command_cleanup.py 2011-07-15 11:50:53 +0000
> +++ identityprovider/tests/test_command_cleanup.py 2011-07-26 07:31:31 +0000
> === modified file 'identityprovider/tests/test_forms.py'
> --- identityprovider/tests/test_forms.py 2011-07-15 11:50:53 +0000
> +++ identityprovider/tests/test_forms.py 2011-07-26 07:31:31 +0000
> @@ -18,6 +18,7 @@
> )
> from identityprovider.api10.forms import WebserviceCreateAccountForm
> from identityprovider.models.openidmodels import OpenIDRPConfig
> +from identityprovider.tests.utils import patch_settings
>
> from .utils import SSOBaseTestCase
>
> @@ -114,6 +115,23 @@
> self.assertTrue(form.is_valid())
> self.assertEqual(form.cleaned_da...

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

I agree with you about the danger of unsafe regexps. An accidental ".*" and captcha verification is always bypassed. :-o

> The only thought I had was whether it should be *outside* of captcha.verify(),
> rather than being hidden within, but that's just a small point - nothing to
> delay the branch landing over :)

Well, captcha.verify(...) is called from multiple places, so putting this into the verification seemed both "correct" (it is part of the verification) *and* simplest (only one place to change and update).

> Great to have the above, but I couldn't see why it was necessary given the
two tests below.

That was the test I wrote first. I think I was thinking that I could then just test .check_whitelist directly as I had already tested that verify used it. It didn't work out like that though.

I'm not entirely sure why we have dependencies in setup.py *and* in requirements.txt. I think it is because setup.py is used to build packages, but requirements.txt isn't.

> I don't see why you don't just use `with patch.object(Captcha, '_open')...`

Yeah, that would be better. I was just fixing some code that was a bit "odd". Just using patch.object would have been better...

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

>> === modified file 'setup.py'
>> --- setup.py 2011-07-06 17:36:56 +0000
>> +++ setup.py 2011-07-26 07:31:31 +0000
>> @@ -39,7 +39,7 @@
>> 'NoseDjango==0.8.1',
>> 'nosexcover',
>> 'coverage==3.4',
>> - 'mock==0.6.0',
>> + 'mock==0.7.2',
>
> Why is this even in the setup.py? I thought (probably wrongly) that mock or
> nose or wsgi-intercept needed to be in the requirements.txt for setting up the dev environment,
> but not in the setup.py, as there's no need for them to be installed? I assume
> that's wrong then (on my part)? Do python packages install their test
> dependencies so they can be tested after install? (is that possible with our
> packages?)

This I can explain. These dependencies are test dependencies, and marked
so (using tests_require).

Having them marked as test dependencies, does not require you to have
them installed for running/installing your product, but allows
setuptools to look for them/install them if necessary when running your
tests via "python setup.py test".
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk4uqS0ACgkQaHF+Qaymu6cPeQCdEjUK/n0pFqSLyYugqF93ucRD
xTUAnR+O1KaXze2NtCda/rZ3n2M+iQvq
=niKk
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'identityprovider/api10/forms.py'
2--- identityprovider/api10/forms.py 2011-07-06 21:14:28 +0000
3+++ identityprovider/api10/forms.py 2011-07-25 20:09:28 +0000
4@@ -59,8 +59,8 @@
5 remote_ip = cleaned_data['remote_ip']
6
7 captcha = Captcha(captcha_id)
8-
9- if captcha.verify(captcha_solution, remote_ip):
10+ email = cleaned_data.get('email', '')
11+ if captcha.verify(captcha_solution, remote_ip, email):
12 return cleaned_data
13 # not verified
14 raise forms.ValidationError(_("Wrong captcha solution."))
15
16=== modified file 'identityprovider/models/captcha.py'
17--- identityprovider/models/captcha.py 2011-06-30 04:55:13 +0000
18+++ identityprovider/models/captcha.py 2011-07-25 20:09:28 +0000
19@@ -66,14 +66,15 @@
20
21 As is verifying received solution:
22
23- >>> captcha = Captcha('captcha-id-received-from-client')
24- >>> captcha.verify("this-is-invalid-solution", ip_addr)
25+ >>> email = 'foo@email.com'
26+ >>> captcha = Captcha('captcha-id-received-from-client', email)
27+ >>> captcha.verify("this-is-invalid-solution", ip_addr, email)
28 False
29
30 Once verified solution is cached, so calling again to .verify() method is
31 very cheap (and returns same result):
32
33- >>> captcha.verify("this-is-invalid-solution", ip_addr)
34+ >>> captcha.verify("this-is-invalid-solution", ip_addr, email)
35 False
36
37 You can also get original response from reCaptcha:
38@@ -159,7 +160,7 @@
39
40 return CaptchaResponse(response.code, response, None)
41
42- def verify(self, captcha_solution, remote_ip):
43+ def verify(self, captcha_solution, remote_ip, email):
44 if self._verified is not None:
45 return self._verified
46
47@@ -167,6 +168,9 @@
48 self.response = None
49 return True
50
51+ if self.check_whitelist(email):
52+ return True
53+
54 if isinstance(captcha_solution, unicode):
55 captcha_solution = captcha_solution.encode('utf-8')
56
57@@ -190,3 +194,9 @@
58 self._verified = False
59 raise VerifyCaptchaError(self.response.traceback)
60 return self._verified
61+
62+ def check_whitelist(self, email):
63+ for pattern in settings.EMAIL_WHITELIST_REGEXP_LIST:
64+ if re.match(pattern, email):
65+ return True
66+ return False
67
68=== modified file 'identityprovider/schema.py'
69--- identityprovider/schema.py 2011-07-12 09:41:08 +0000
70+++ identityprovider/schema.py 2011-07-25 20:09:28 +0000
71@@ -78,6 +78,9 @@
72 captcha.captcha_public_key = StringConfigOption()
73 captcha.captcha_private_key = StringConfigOption()
74 captcha.disable_captcha_verification = BoolConfigOption()
75+ captcha.email_whitelist_regexp_list = LinesConfigOption(
76+ item=StringConfigOption(raw=True)
77+ )
78
79 # debug
80 debug = ConfigSection()
81
82=== modified file 'identityprovider/tests/test_auth.py'
83--- identityprovider/tests/test_auth.py 2011-07-15 15:17:19 +0000
84+++ identityprovider/tests/test_auth.py 2011-07-25 20:09:28 +0000
85@@ -5,7 +5,6 @@
86 from mock import (
87 Mock,
88 patch,
89- patch_object,
90 )
91 from oauth_backend.models import Consumer
92 from piston.oauth import OAuthError as PistonOAuthError
93@@ -171,19 +170,19 @@
94
95 def test_is_authenticated_invalid_request(self):
96 mock_is_valid_request = Mock(return_value=False)
97- with patch_object(self.auth, 'is_valid_request', mock_is_valid_request):
98+ with patch.object(self.auth, 'is_valid_request', mock_is_valid_request):
99 self.assertEqual(self.auth.is_authenticated(self.request), False)
100
101 def test_is_authenticated_piston_oauth_error(self):
102 mock_validate_token = Mock(side_effect=PistonOAuthError)
103- with patch_object(self.auth, 'validate_token', mock_validate_token):
104+ with patch.object(self.auth, 'validate_token', mock_validate_token):
105 self.assertEqual(self.auth.is_authenticated(self.request), False)
106
107 def test_is_authenticated_no_consumer_token(self):
108 consumer, token, params = None, None, None
109 mock_validate_token = Mock(return_value=(consumer, token, params))
110 # authentication fails as there is no consumer and token in the request
111- with patch_object(self.auth, 'validate_token', mock_validate_token):
112+ with patch.object(self.auth, 'validate_token', mock_validate_token):
113 self.assertEqual(self.auth.is_authenticated(self.request), False)
114
115 @patch('identityprovider.auth.Account.objects.get')
116@@ -194,7 +193,7 @@
117 consumer, token, params = Mock(), Mock(), None
118 # make sure validate_token returns a consumer and a token
119 mock_validate_token = Mock(return_value=(consumer, token, params))
120- with patch_object(self.auth, 'validate_token', mock_validate_token):
121+ with patch.object(self.auth, 'validate_token', mock_validate_token):
122 self.assertEqual(self.auth.is_authenticated(self.request), False)
123
124 @patch('identityprovider.auth.Account.objects.get')
125@@ -206,7 +205,7 @@
126 consumer, params = Mock(), None
127 # make sure validate_token returns a consumer and a token
128 mock_validate_token = Mock(return_value=(consumer, mock_token, params))
129- with patch_object(self.auth, 'validate_token', mock_validate_token):
130+ with patch.object(self.auth, 'validate_token', mock_validate_token):
131 response = self.auth.is_authenticated(self.request)
132
133 self.assertEqual(response, True)
134
135=== modified file 'identityprovider/tests/test_captcha.py'
136--- identityprovider/tests/test_captcha.py 2011-07-21 16:08:06 +0000
137+++ identityprovider/tests/test_captcha.py 2011-07-25 20:09:28 +0000
138@@ -27,7 +27,7 @@
139 captcha = Captcha(None)
140 captcha._open = Mock()
141 captcha._open.return_value.is_error = True
142- captcha.verify(captcha_solution, 'foobar')
143+ captcha.verify(captcha_solution, 'foobar', '')
144
145 captcha_response = mock_urlencode.call_args[0][0]['response']
146 self.assertEqual(captcha_response, encoded_solution)
147@@ -59,7 +59,7 @@
148 def test_verify_is_short_circuited_when_disabled_in_settings(self):
149 with patch_settings(DISABLE_CAPTCHA_VERIFICATION=True):
150 captcha = Captcha('test')
151- verify_result = captcha.verify('solution', '127.0.0.1')
152+ verify_result = captcha.verify('solution', '127.0.0.1', '')
153
154 self.assertTrue(verify_result)
155 self.assertTrue(captcha.response is None)
156
157=== modified file 'identityprovider/tests/test_command_cleanup.py'
158--- identityprovider/tests/test_command_cleanup.py 2011-07-15 11:50:53 +0000
159+++ identityprovider/tests/test_command_cleanup.py 2011-07-25 20:09:28 +0000
160@@ -8,7 +8,7 @@
161 from django.core.management import call_command
162 from django.db import connection
163 from django.test import TestCase
164-from mock import patch_object
165+from mock import patch
166
167 from identityprovider.models import OpenIDNonce
168
169@@ -34,7 +34,7 @@
170 self.populate()
171
172 mock_stdout = StringIO()
173- with patch_object(sys, 'stdout', mock_stdout):
174+ with patch.object(sys, 'stdout', mock_stdout):
175 call_command('cleanup', verbosity=0)
176 mock_stdout.seek(0)
177 self.assertTrue('No items selected to clean up.' in mock_stdout.read())
178
179=== modified file 'identityprovider/tests/test_forms.py'
180--- identityprovider/tests/test_forms.py 2011-07-15 11:50:53 +0000
181+++ identityprovider/tests/test_forms.py 2011-07-25 20:09:28 +0000
182@@ -18,6 +18,7 @@
183 )
184 from identityprovider.api10.forms import WebserviceCreateAccountForm
185 from identityprovider.models.openidmodels import OpenIDRPConfig
186+from identityprovider.tests.utils import patch_settings
187
188 from .utils import SSOBaseTestCase
189
190@@ -114,6 +115,23 @@
191 self.assertTrue(form.is_valid())
192 self.assertEqual(form.cleaned_data['platform'], 'desktop')
193
194+ def test_captcha_checked_for_whitelist(self):
195+ data = {
196+ 'email': 'canonicaltest@gmail.com',
197+ 'password': 'password1A',
198+ 'captcha_id': '1',
199+ 'captcha_solution': '2',
200+ 'remote_ip': '127.0.0.1',
201+ }
202+ pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
203+ overrides = dict(
204+ DISABLE_CAPTCHA_VERIFICATION=False,
205+ EMAIL_WHITELIST_REGEXP_LIST=[pattern],
206+ )
207+ with patch_settings(**overrides):
208+ form = WebserviceCreateAccountForm(data)
209+ self.assertTrue(form.is_valid())
210+
211 def test_default_cleaned_validate_redirect_to(self):
212 data = {
213 'email': 'some@email.com',
214
215=== modified file 'identityprovider/tests/test_models_captcha.py'
216--- identityprovider/tests/test_models_captcha.py 2011-01-20 21:48:43 +0000
217+++ identityprovider/tests/test_models_captcha.py 2011-07-25 20:09:28 +0000
218@@ -4,7 +4,10 @@
219 import unittest
220 import urllib2
221
222-from mock import patch_object
223+from mock import (
224+ Mock,
225+ patch,
226+)
227
228 from cStringIO import StringIO
229 from django.conf import settings
230@@ -16,6 +19,8 @@
231 VerifyCaptchaError,
232 )
233
234+from identityprovider.tests.utils import patch_settings
235+
236
237 class CaptchaResponseTestCase(unittest.TestCase):
238
239@@ -37,11 +42,11 @@
240
241 class CaptchaTestCase(unittest.TestCase):
242
243- @patch_object(Captcha, '_open')
244+ @patch.object(Captcha, '_open')
245 def test_new_captcha_error(self, mock_open):
246 self.assertRaises(NewCaptchaError, Captcha.new)
247
248- @patch_object(Captcha, '_open')
249+ @patch.object(Captcha, '_open')
250 def test_new_captcha_no_challenge(self, mock_open):
251 try:
252 Captcha.new()
253@@ -70,23 +75,56 @@
254 def tearDown(self):
255 settings.DISABLE_CAPTCHA_VERIFICATION = self.old_DISABLE_CAPTCHA_VERIFICATION
256
257- @patch_object(Captcha, '_open')
258+ @patch.object(Captcha, '_open')
259+ def test_verify_calls_check_whitelist(self, mock_open):
260+ captcha = Captcha(None)
261+ captcha.check_whitelist = Mock(return_value=False)
262+
263+ result = captcha.verify(None, 'localhost', 'foo')
264+ self.assertFalse(result)
265+ self.assertTrue(mock_open.called)
266+
267+ captcha.check_whitelist.assert_called_with('foo')
268+
269+ @patch.object(Captcha, '_open')
270+ def test_check_whitelist_match(self, mock_open):
271+ captcha = Captcha(None)
272+
273+ regexps = ['not a match', '^foo$']
274+ with patch_settings(
275+ EMAIL_WHITELIST_REGEXP_LIST=regexps
276+ ):
277+ result = captcha.verify(None, 'localhost', 'foo')
278+ self.assertTrue(result)
279+ self.assertFalse(mock_open.called)
280+
281+ @patch.object(Captcha, '_open')
282+ def test_check_whitelist_no_match(self, mock_open):
283+ captcha = Captcha(None)
284+
285+ regexps = ['not a match', '^bar$']
286+ with patch_settings(
287+ EMAIL_WHITELIST_REGEXP_LIST=regexps
288+ ):
289+ result = captcha.verify(None, 'localhost', 'foo')
290+ self.assertFalse(result)
291+ self.assertTrue(mock_open.called)
292+
293+ @patch.object(Captcha, '_open')
294 def test_verify_already_verified(self, mock_open):
295 c = Captcha(None)
296 c._verified = True
297- r = c.verify(None, None)
298+ r = c.verify(None, None, '')
299 self.assertTrue(r)
300
301- @patch_object(Captcha, '_open')
302+ @patch.object(Captcha, '_open')
303 def test_verify_no_verification(self, mock_open):
304- settings.DISABLE_CAPTCHA_VERIFICATION = True
305-
306- c = Captcha(None)
307- r = c.verify(None, None)
308- self.assertTrue(r)
309-
310- @patch_object(Captcha, '_open')
311- def test_verify_response_ok(self, mock_open):
312+ with patch_settings(DISABLE_CAPTCHA_VERIFICATION=True):
313+ c = Captcha(None)
314+ r = c.verify(None, None, '')
315+ self.assertTrue(r)
316+
317+ def test_verify_response_ok(self):
318 @classmethod
319 def mock_open(cls, request):
320 r = CaptchaResponse(200, StringIO('true\nok'))
321@@ -94,25 +132,28 @@
322 old_open = Captcha._open
323 Captcha._open = mock_open
324
325- c = Captcha.new()
326- r = c.verify(None, "localhost")
327-
328- self.assertTrue(r)
329- self.assertEqual(c.message, 'ok')
330-
331- Captcha._open = old_open
332-
333- @patch_object(Captcha, '_open')
334+ try:
335+ c = Captcha.new()
336+ r = c.verify(None, "localhost", '')
337+
338+ self.assertTrue(r)
339+ self.assertEqual(c.message, 'ok')
340+ finally:
341+ Captcha._open = old_open
342+
343+ @patch.object(Captcha, '_open')
344 def test_verify_no_captcha_id(self, mock_open):
345 c = Captcha(None)
346- r = c.verify(None, 'localhost')
347+ r = c.verify(None, 'localhost', '')
348
349 self.assertFalse(r)
350 self.assertEqual(c.message, 'no-challenge')
351
352- @patch_object(Captcha, '_open')
353+ @patch.object(Captcha, '_open')
354 def test_verify_error(self, mock_open):
355 c = Captcha(None)
356 c.captcha_id = 'challenge-id'
357
358- self.assertRaises(VerifyCaptchaError, c.verify, None, 'localhost')
359+ self.assertRaises(
360+ VerifyCaptchaError, c.verify, None, 'localhost', ''
361+ )
362
363=== modified file 'identityprovider/tests/test_views_ui.py'
364--- identityprovider/tests/test_views_ui.py 2011-07-19 14:21:42 +0000
365+++ identityprovider/tests/test_views_ui.py 2011-07-25 20:09:28 +0000
366@@ -7,7 +7,7 @@
367 import urllib2
368 from urlparse import urlsplit
369
370-from mock import patch_object, patch
371+from mock import patch
372
373 from django.conf import settings
374 from django.core import mail
375@@ -30,6 +30,7 @@
376 from identityprovider.tests.utils import (
377 LPAccountTestCase,
378 MockHandler,
379+ patch_settings,
380 )
381 from identityprovider.utils import is_django_13
382
383@@ -592,7 +593,7 @@
384 def __init__(self, *args):
385 pass
386
387- def verify(self, solution, ip_addr):
388+ def verify(self, solution, ip_addr, email):
389 self.message = 'no-challenge'
390 return False
391
392@@ -635,7 +636,7 @@
393 self.assertEqual(len(mail.outbox), mails_sent)
394 mail.outbox = []
395
396- @patch_object(logging.Logger, 'debug')
397+ @patch.object(logging.Logger, 'debug')
398 def test_new_account_when_person_exists_and_account_not(self, mock_debug):
399 person = Person.objects.create(displayname='Person')
400 EmailAddress.objects.create(email='person@example.com',
401@@ -815,6 +816,55 @@
402 msg = 'It appears that our captcha service was unable to load'
403 self.assertContains(r, msg)
404
405+ def test_new_account_captcha_whitelist(self):
406+ email = 'canonicaltest@gmail.com'
407+ pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
408+ overrides = dict(
409+ DISABLE_CAPTCHA_VERIFICATION=False,
410+ EMAIL_WHITELIST_REGEXP_LIST=[pattern],
411+ )
412+ with patch_settings(**overrides):
413+ response = _post_new_account(
414+ self.client,
415+ email=email
416+ )
417+ # should redirect to '/+email-sent'
418+ self.assertEqual(response.status_code, 302)
419+ redirect = response['location']
420+ self.assertTrue(redirect.endswith('/+email-sent'))
421+
422+ def test_new_account_captcha_whitelist_with_uuid(self):
423+ email = 'canonicaltest+something@gmail.com'
424+ pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
425+ overrides = dict(
426+ DISABLE_CAPTCHA_VERIFICATION=False,
427+ EMAIL_WHITELIST_REGEXP_LIST=[pattern],
428+ )
429+ with patch_settings(**overrides):
430+ response = _post_new_account(
431+ self.client,
432+ email=email
433+ )
434+ # should redirect to '/+email-sent'
435+ self.assertEqual(response.status_code, 302)
436+ redirect = response['location']
437+ self.assertTrue(redirect.endswith('/+email-sent'))
438+
439+ def test_new_account_captcha_whitelist_fail(self):
440+ email = 'notcanonicaltest@gmail.com'
441+ pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
442+ overrides = dict(
443+ DISABLE_CAPTCHA_VERIFICATION=False,
444+ EMAIL_WHITELIST_REGEXP_LIST=[pattern],
445+ )
446+ with patch_settings(**overrides):
447+ response = _post_new_account(
448+ self.client,
449+ email=email
450+ )
451+ msg = 'It appears that our captcha service was unable to load'
452+ self.assertContains(response, msg)
453+
454
455 class LanguagesTestCase(SSOBaseTestCase):
456
457
458=== modified file 'identityprovider/views/ui.py'
459--- identityprovider/views/ui.py 2011-07-21 16:08:06 +0000
460+++ identityprovider/views/ui.py 2011-07-25 20:09:28 +0000
461@@ -464,9 +464,10 @@
462 def _verify_captcha_response(template, request, form):
463 captcha = Captcha(request.POST.get('recaptcha_challenge_field'))
464 captcha_solution = request.POST.get('recaptcha_response_field')
465+ email = request.POST.get('email', '')
466 ip_addr = request.environ["REMOTE_ADDR"]
467 try:
468- verified = captcha.verify(captcha_solution, ip_addr)
469+ verified = captcha.verify(captcha_solution, ip_addr, email)
470 if verified:
471 return None
472 except VerifyCaptchaError, e:
473
474=== modified file 'requirements.txt'
475--- requirements.txt 2011-07-12 14:38:02 +0000
476+++ requirements.txt 2011-07-25 20:09:28 +0000
477@@ -6,7 +6,7 @@
478 NoseDjango==0.8.1
479 nosexcover
480 coverage==3.4
481-mock==0.6.0
482+mock==0.7.2
483 wsgiref==0.1.2
484 wsgi-intercept==0.4
485 zope.testbrowser==3.5.1
486
487=== modified file 'setup.py'
488--- setup.py 2011-07-06 17:36:56 +0000
489+++ setup.py 2011-07-25 20:09:28 +0000
490@@ -39,7 +39,7 @@
491 'NoseDjango==0.8.1',
492 'nosexcover',
493 'coverage==3.4',
494- 'mock==0.6.0',
495+ 'mock==0.7.2',
496 'wsgiref==0.1.2',
497 'wsgi-intercept==0.4',
498 'zope.testbrowser==3.5.1',