Merge lp:~canonical-isd-hackers/canonical-identity-provider/email-whitelist into lp:canonical-identity-provider/release
- email-whitelist
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | code | Approve | |
Review via email:
|
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
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
This mp adds an EMAIL_WHITELIST
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:
^canonicalt
So in local.cfg add:
[captcha]
email_whitelist
^canonicalt
All tests pass in this branch.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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(
Yeah, that would be better. I was just fixing some code that was a bit "odd". Just using patch.object would have been better...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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=
>> '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://
iEYEARECAAYFAk4
xTUAnR+
=niKk
-----END PGP SIGNATURE-----
Preview Diff
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 | 59 | remote_ip = cleaned_data['remote_ip'] | 59 | remote_ip = cleaned_data['remote_ip'] |
6 | 60 | 60 | ||
7 | 61 | captcha = Captcha(captcha_id) | 61 | captcha = Captcha(captcha_id) |
10 | 62 | 62 | email = cleaned_data.get('email', '') | |
11 | 63 | if captcha.verify(captcha_solution, remote_ip): | 63 | if captcha.verify(captcha_solution, remote_ip, email): |
12 | 64 | return cleaned_data | 64 | return cleaned_data |
13 | 65 | # not verified | 65 | # not verified |
14 | 66 | raise forms.ValidationError(_("Wrong captcha solution.")) | 66 | raise forms.ValidationError(_("Wrong captcha solution.")) |
15 | 67 | 67 | ||
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 | 66 | 66 | ||
21 | 67 | As is verifying received solution: | 67 | As is verifying received solution: |
22 | 68 | 68 | ||
25 | 69 | >>> captcha = Captcha('captcha-id-received-from-client') | 69 | >>> email = 'foo@email.com' |
26 | 70 | >>> captcha.verify("this-is-invalid-solution", ip_addr) | 70 | >>> captcha = Captcha('captcha-id-received-from-client', email) |
27 | 71 | >>> captcha.verify("this-is-invalid-solution", ip_addr, email) | ||
28 | 71 | False | 72 | False |
29 | 72 | 73 | ||
30 | 73 | Once verified solution is cached, so calling again to .verify() method is | 74 | Once verified solution is cached, so calling again to .verify() method is |
31 | 74 | very cheap (and returns same result): | 75 | very cheap (and returns same result): |
32 | 75 | 76 | ||
34 | 76 | >>> captcha.verify("this-is-invalid-solution", ip_addr) | 77 | >>> captcha.verify("this-is-invalid-solution", ip_addr, email) |
35 | 77 | False | 78 | False |
36 | 78 | 79 | ||
37 | 79 | You can also get original response from reCaptcha: | 80 | You can also get original response from reCaptcha: |
38 | @@ -159,7 +160,7 @@ | |||
39 | 159 | 160 | ||
40 | 160 | return CaptchaResponse(response.code, response, None) | 161 | return CaptchaResponse(response.code, response, None) |
41 | 161 | 162 | ||
43 | 162 | def verify(self, captcha_solution, remote_ip): | 163 | def verify(self, captcha_solution, remote_ip, email): |
44 | 163 | if self._verified is not None: | 164 | if self._verified is not None: |
45 | 164 | return self._verified | 165 | return self._verified |
46 | 165 | 166 | ||
47 | @@ -167,6 +168,9 @@ | |||
48 | 167 | self.response = None | 168 | self.response = None |
49 | 168 | return True | 169 | return True |
50 | 169 | 170 | ||
51 | 171 | if self.check_whitelist(email): | ||
52 | 172 | return True | ||
53 | 173 | |||
54 | 170 | if isinstance(captcha_solution, unicode): | 174 | if isinstance(captcha_solution, unicode): |
55 | 171 | captcha_solution = captcha_solution.encode('utf-8') | 175 | captcha_solution = captcha_solution.encode('utf-8') |
56 | 172 | 176 | ||
57 | @@ -190,3 +194,9 @@ | |||
58 | 190 | self._verified = False | 194 | self._verified = False |
59 | 191 | raise VerifyCaptchaError(self.response.traceback) | 195 | raise VerifyCaptchaError(self.response.traceback) |
60 | 192 | return self._verified | 196 | return self._verified |
61 | 197 | |||
62 | 198 | def check_whitelist(self, email): | ||
63 | 199 | for pattern in settings.EMAIL_WHITELIST_REGEXP_LIST: | ||
64 | 200 | if re.match(pattern, email): | ||
65 | 201 | return True | ||
66 | 202 | return False | ||
67 | 193 | 203 | ||
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 | 78 | captcha.captcha_public_key = StringConfigOption() | 78 | captcha.captcha_public_key = StringConfigOption() |
73 | 79 | captcha.captcha_private_key = StringConfigOption() | 79 | captcha.captcha_private_key = StringConfigOption() |
74 | 80 | captcha.disable_captcha_verification = BoolConfigOption() | 80 | captcha.disable_captcha_verification = BoolConfigOption() |
75 | 81 | captcha.email_whitelist_regexp_list = LinesConfigOption( | ||
76 | 82 | item=StringConfigOption(raw=True) | ||
77 | 83 | ) | ||
78 | 81 | 84 | ||
79 | 82 | # debug | 85 | # debug |
80 | 83 | debug = ConfigSection() | 86 | debug = ConfigSection() |
81 | 84 | 87 | ||
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 | 5 | from mock import ( | 5 | from mock import ( |
87 | 6 | Mock, | 6 | Mock, |
88 | 7 | patch, | 7 | patch, |
89 | 8 | patch_object, | ||
90 | 9 | ) | 8 | ) |
91 | 10 | from oauth_backend.models import Consumer | 9 | from oauth_backend.models import Consumer |
92 | 11 | from piston.oauth import OAuthError as PistonOAuthError | 10 | from piston.oauth import OAuthError as PistonOAuthError |
93 | @@ -171,19 +170,19 @@ | |||
94 | 171 | 170 | ||
95 | 172 | def test_is_authenticated_invalid_request(self): | 171 | def test_is_authenticated_invalid_request(self): |
96 | 173 | mock_is_valid_request = Mock(return_value=False) | 172 | mock_is_valid_request = Mock(return_value=False) |
98 | 174 | with patch_object(self.auth, 'is_valid_request', mock_is_valid_request): | 173 | with patch.object(self.auth, 'is_valid_request', mock_is_valid_request): |
99 | 175 | self.assertEqual(self.auth.is_authenticated(self.request), False) | 174 | self.assertEqual(self.auth.is_authenticated(self.request), False) |
100 | 176 | 175 | ||
101 | 177 | def test_is_authenticated_piston_oauth_error(self): | 176 | def test_is_authenticated_piston_oauth_error(self): |
102 | 178 | mock_validate_token = Mock(side_effect=PistonOAuthError) | 177 | mock_validate_token = Mock(side_effect=PistonOAuthError) |
104 | 179 | with patch_object(self.auth, 'validate_token', mock_validate_token): | 178 | with patch.object(self.auth, 'validate_token', mock_validate_token): |
105 | 180 | self.assertEqual(self.auth.is_authenticated(self.request), False) | 179 | self.assertEqual(self.auth.is_authenticated(self.request), False) |
106 | 181 | 180 | ||
107 | 182 | def test_is_authenticated_no_consumer_token(self): | 181 | def test_is_authenticated_no_consumer_token(self): |
108 | 183 | consumer, token, params = None, None, None | 182 | consumer, token, params = None, None, None |
109 | 184 | mock_validate_token = Mock(return_value=(consumer, token, params)) | 183 | mock_validate_token = Mock(return_value=(consumer, token, params)) |
110 | 185 | # authentication fails as there is no consumer and token in the request | 184 | # authentication fails as there is no consumer and token in the request |
112 | 186 | with patch_object(self.auth, 'validate_token', mock_validate_token): | 185 | with patch.object(self.auth, 'validate_token', mock_validate_token): |
113 | 187 | self.assertEqual(self.auth.is_authenticated(self.request), False) | 186 | self.assertEqual(self.auth.is_authenticated(self.request), False) |
114 | 188 | 187 | ||
115 | 189 | @patch('identityprovider.auth.Account.objects.get') | 188 | @patch('identityprovider.auth.Account.objects.get') |
116 | @@ -194,7 +193,7 @@ | |||
117 | 194 | consumer, token, params = Mock(), Mock(), None | 193 | consumer, token, params = Mock(), Mock(), None |
118 | 195 | # make sure validate_token returns a consumer and a token | 194 | # make sure validate_token returns a consumer and a token |
119 | 196 | mock_validate_token = Mock(return_value=(consumer, token, params)) | 195 | mock_validate_token = Mock(return_value=(consumer, token, params)) |
121 | 197 | with patch_object(self.auth, 'validate_token', mock_validate_token): | 196 | with patch.object(self.auth, 'validate_token', mock_validate_token): |
122 | 198 | self.assertEqual(self.auth.is_authenticated(self.request), False) | 197 | self.assertEqual(self.auth.is_authenticated(self.request), False) |
123 | 199 | 198 | ||
124 | 200 | @patch('identityprovider.auth.Account.objects.get') | 199 | @patch('identityprovider.auth.Account.objects.get') |
125 | @@ -206,7 +205,7 @@ | |||
126 | 206 | consumer, params = Mock(), None | 205 | consumer, params = Mock(), None |
127 | 207 | # make sure validate_token returns a consumer and a token | 206 | # make sure validate_token returns a consumer and a token |
128 | 208 | mock_validate_token = Mock(return_value=(consumer, mock_token, params)) | 207 | mock_validate_token = Mock(return_value=(consumer, mock_token, params)) |
130 | 209 | with patch_object(self.auth, 'validate_token', mock_validate_token): | 208 | with patch.object(self.auth, 'validate_token', mock_validate_token): |
131 | 210 | response = self.auth.is_authenticated(self.request) | 209 | response = self.auth.is_authenticated(self.request) |
132 | 211 | 210 | ||
133 | 212 | self.assertEqual(response, True) | 211 | self.assertEqual(response, True) |
134 | 213 | 212 | ||
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 | 27 | captcha = Captcha(None) | 27 | captcha = Captcha(None) |
140 | 28 | captcha._open = Mock() | 28 | captcha._open = Mock() |
141 | 29 | captcha._open.return_value.is_error = True | 29 | captcha._open.return_value.is_error = True |
143 | 30 | captcha.verify(captcha_solution, 'foobar') | 30 | captcha.verify(captcha_solution, 'foobar', '') |
144 | 31 | 31 | ||
145 | 32 | captcha_response = mock_urlencode.call_args[0][0]['response'] | 32 | captcha_response = mock_urlencode.call_args[0][0]['response'] |
146 | 33 | self.assertEqual(captcha_response, encoded_solution) | 33 | self.assertEqual(captcha_response, encoded_solution) |
147 | @@ -59,7 +59,7 @@ | |||
148 | 59 | def test_verify_is_short_circuited_when_disabled_in_settings(self): | 59 | def test_verify_is_short_circuited_when_disabled_in_settings(self): |
149 | 60 | with patch_settings(DISABLE_CAPTCHA_VERIFICATION=True): | 60 | with patch_settings(DISABLE_CAPTCHA_VERIFICATION=True): |
150 | 61 | captcha = Captcha('test') | 61 | captcha = Captcha('test') |
152 | 62 | verify_result = captcha.verify('solution', '127.0.0.1') | 62 | verify_result = captcha.verify('solution', '127.0.0.1', '') |
153 | 63 | 63 | ||
154 | 64 | self.assertTrue(verify_result) | 64 | self.assertTrue(verify_result) |
155 | 65 | self.assertTrue(captcha.response is None) | 65 | self.assertTrue(captcha.response is None) |
156 | 66 | 66 | ||
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 | 8 | from django.core.management import call_command | 8 | from django.core.management import call_command |
162 | 9 | from django.db import connection | 9 | from django.db import connection |
163 | 10 | from django.test import TestCase | 10 | from django.test import TestCase |
165 | 11 | from mock import patch_object | 11 | from mock import patch |
166 | 12 | 12 | ||
167 | 13 | from identityprovider.models import OpenIDNonce | 13 | from identityprovider.models import OpenIDNonce |
168 | 14 | 14 | ||
169 | @@ -34,7 +34,7 @@ | |||
170 | 34 | self.populate() | 34 | self.populate() |
171 | 35 | 35 | ||
172 | 36 | mock_stdout = StringIO() | 36 | mock_stdout = StringIO() |
174 | 37 | with patch_object(sys, 'stdout', mock_stdout): | 37 | with patch.object(sys, 'stdout', mock_stdout): |
175 | 38 | call_command('cleanup', verbosity=0) | 38 | call_command('cleanup', verbosity=0) |
176 | 39 | mock_stdout.seek(0) | 39 | mock_stdout.seek(0) |
177 | 40 | self.assertTrue('No items selected to clean up.' in mock_stdout.read()) | 40 | self.assertTrue('No items selected to clean up.' in mock_stdout.read()) |
178 | 41 | 41 | ||
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 | 18 | ) | 18 | ) |
184 | 19 | from identityprovider.api10.forms import WebserviceCreateAccountForm | 19 | from identityprovider.api10.forms import WebserviceCreateAccountForm |
185 | 20 | from identityprovider.models.openidmodels import OpenIDRPConfig | 20 | from identityprovider.models.openidmodels import OpenIDRPConfig |
186 | 21 | from identityprovider.tests.utils import patch_settings | ||
187 | 21 | 22 | ||
188 | 22 | from .utils import SSOBaseTestCase | 23 | from .utils import SSOBaseTestCase |
189 | 23 | 24 | ||
190 | @@ -114,6 +115,23 @@ | |||
191 | 114 | self.assertTrue(form.is_valid()) | 115 | self.assertTrue(form.is_valid()) |
192 | 115 | self.assertEqual(form.cleaned_data['platform'], 'desktop') | 116 | self.assertEqual(form.cleaned_data['platform'], 'desktop') |
193 | 116 | 117 | ||
194 | 118 | def test_captcha_checked_for_whitelist(self): | ||
195 | 119 | data = { | ||
196 | 120 | 'email': 'canonicaltest@gmail.com', | ||
197 | 121 | 'password': 'password1A', | ||
198 | 122 | 'captcha_id': '1', | ||
199 | 123 | 'captcha_solution': '2', | ||
200 | 124 | 'remote_ip': '127.0.0.1', | ||
201 | 125 | } | ||
202 | 126 | pattern = '^canonicaltest(?:\+.+)?@gmail\.com$' | ||
203 | 127 | overrides = dict( | ||
204 | 128 | DISABLE_CAPTCHA_VERIFICATION=False, | ||
205 | 129 | EMAIL_WHITELIST_REGEXP_LIST=[pattern], | ||
206 | 130 | ) | ||
207 | 131 | with patch_settings(**overrides): | ||
208 | 132 | form = WebserviceCreateAccountForm(data) | ||
209 | 133 | self.assertTrue(form.is_valid()) | ||
210 | 134 | |||
211 | 117 | def test_default_cleaned_validate_redirect_to(self): | 135 | def test_default_cleaned_validate_redirect_to(self): |
212 | 118 | data = { | 136 | data = { |
213 | 119 | 'email': 'some@email.com', | 137 | 'email': 'some@email.com', |
214 | 120 | 138 | ||
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 | 4 | import unittest | 4 | import unittest |
220 | 5 | import urllib2 | 5 | import urllib2 |
221 | 6 | 6 | ||
223 | 7 | from mock import patch_object | 7 | from mock import ( |
224 | 8 | Mock, | ||
225 | 9 | patch, | ||
226 | 10 | ) | ||
227 | 8 | 11 | ||
228 | 9 | from cStringIO import StringIO | 12 | from cStringIO import StringIO |
229 | 10 | from django.conf import settings | 13 | from django.conf import settings |
230 | @@ -16,6 +19,8 @@ | |||
231 | 16 | VerifyCaptchaError, | 19 | VerifyCaptchaError, |
232 | 17 | ) | 20 | ) |
233 | 18 | 21 | ||
234 | 22 | from identityprovider.tests.utils import patch_settings | ||
235 | 23 | |||
236 | 19 | 24 | ||
237 | 20 | class CaptchaResponseTestCase(unittest.TestCase): | 25 | class CaptchaResponseTestCase(unittest.TestCase): |
238 | 21 | 26 | ||
239 | @@ -37,11 +42,11 @@ | |||
240 | 37 | 42 | ||
241 | 38 | class CaptchaTestCase(unittest.TestCase): | 43 | class CaptchaTestCase(unittest.TestCase): |
242 | 39 | 44 | ||
244 | 40 | @patch_object(Captcha, '_open') | 45 | @patch.object(Captcha, '_open') |
245 | 41 | def test_new_captcha_error(self, mock_open): | 46 | def test_new_captcha_error(self, mock_open): |
246 | 42 | self.assertRaises(NewCaptchaError, Captcha.new) | 47 | self.assertRaises(NewCaptchaError, Captcha.new) |
247 | 43 | 48 | ||
249 | 44 | @patch_object(Captcha, '_open') | 49 | @patch.object(Captcha, '_open') |
250 | 45 | def test_new_captcha_no_challenge(self, mock_open): | 50 | def test_new_captcha_no_challenge(self, mock_open): |
251 | 46 | try: | 51 | try: |
252 | 47 | Captcha.new() | 52 | Captcha.new() |
253 | @@ -70,23 +75,56 @@ | |||
254 | 70 | def tearDown(self): | 75 | def tearDown(self): |
255 | 71 | settings.DISABLE_CAPTCHA_VERIFICATION = self.old_DISABLE_CAPTCHA_VERIFICATION | 76 | settings.DISABLE_CAPTCHA_VERIFICATION = self.old_DISABLE_CAPTCHA_VERIFICATION |
256 | 72 | 77 | ||
258 | 73 | @patch_object(Captcha, '_open') | 78 | @patch.object(Captcha, '_open') |
259 | 79 | def test_verify_calls_check_whitelist(self, mock_open): | ||
260 | 80 | captcha = Captcha(None) | ||
261 | 81 | captcha.check_whitelist = Mock(return_value=False) | ||
262 | 82 | |||
263 | 83 | result = captcha.verify(None, 'localhost', 'foo') | ||
264 | 84 | self.assertFalse(result) | ||
265 | 85 | self.assertTrue(mock_open.called) | ||
266 | 86 | |||
267 | 87 | captcha.check_whitelist.assert_called_with('foo') | ||
268 | 88 | |||
269 | 89 | @patch.object(Captcha, '_open') | ||
270 | 90 | def test_check_whitelist_match(self, mock_open): | ||
271 | 91 | captcha = Captcha(None) | ||
272 | 92 | |||
273 | 93 | regexps = ['not a match', '^foo$'] | ||
274 | 94 | with patch_settings( | ||
275 | 95 | EMAIL_WHITELIST_REGEXP_LIST=regexps | ||
276 | 96 | ): | ||
277 | 97 | result = captcha.verify(None, 'localhost', 'foo') | ||
278 | 98 | self.assertTrue(result) | ||
279 | 99 | self.assertFalse(mock_open.called) | ||
280 | 100 | |||
281 | 101 | @patch.object(Captcha, '_open') | ||
282 | 102 | def test_check_whitelist_no_match(self, mock_open): | ||
283 | 103 | captcha = Captcha(None) | ||
284 | 104 | |||
285 | 105 | regexps = ['not a match', '^bar$'] | ||
286 | 106 | with patch_settings( | ||
287 | 107 | EMAIL_WHITELIST_REGEXP_LIST=regexps | ||
288 | 108 | ): | ||
289 | 109 | result = captcha.verify(None, 'localhost', 'foo') | ||
290 | 110 | self.assertFalse(result) | ||
291 | 111 | self.assertTrue(mock_open.called) | ||
292 | 112 | |||
293 | 113 | @patch.object(Captcha, '_open') | ||
294 | 74 | def test_verify_already_verified(self, mock_open): | 114 | def test_verify_already_verified(self, mock_open): |
295 | 75 | c = Captcha(None) | 115 | c = Captcha(None) |
296 | 76 | c._verified = True | 116 | c._verified = True |
298 | 77 | r = c.verify(None, None) | 117 | r = c.verify(None, None, '') |
299 | 78 | self.assertTrue(r) | 118 | self.assertTrue(r) |
300 | 79 | 119 | ||
302 | 80 | @patch_object(Captcha, '_open') | 120 | @patch.object(Captcha, '_open') |
303 | 81 | def test_verify_no_verification(self, mock_open): | 121 | def test_verify_no_verification(self, mock_open): |
312 | 82 | settings.DISABLE_CAPTCHA_VERIFICATION = True | 122 | with patch_settings(DISABLE_CAPTCHA_VERIFICATION=True): |
313 | 83 | 123 | c = Captcha(None) | |
314 | 84 | c = Captcha(None) | 124 | r = c.verify(None, None, '') |
315 | 85 | r = c.verify(None, None) | 125 | self.assertTrue(r) |
316 | 86 | self.assertTrue(r) | 126 | |
317 | 87 | 127 | def test_verify_response_ok(self): | |
310 | 88 | @patch_object(Captcha, '_open') | ||
311 | 89 | def test_verify_response_ok(self, mock_open): | ||
318 | 90 | @classmethod | 128 | @classmethod |
319 | 91 | def mock_open(cls, request): | 129 | def mock_open(cls, request): |
320 | 92 | r = CaptchaResponse(200, StringIO('true\nok')) | 130 | r = CaptchaResponse(200, StringIO('true\nok')) |
321 | @@ -94,25 +132,28 @@ | |||
322 | 94 | old_open = Captcha._open | 132 | old_open = Captcha._open |
323 | 95 | Captcha._open = mock_open | 133 | Captcha._open = mock_open |
324 | 96 | 134 | ||
334 | 97 | c = Captcha.new() | 135 | try: |
335 | 98 | r = c.verify(None, "localhost") | 136 | c = Captcha.new() |
336 | 99 | 137 | r = c.verify(None, "localhost", '') | |
337 | 100 | self.assertTrue(r) | 138 | |
338 | 101 | self.assertEqual(c.message, 'ok') | 139 | self.assertTrue(r) |
339 | 102 | 140 | self.assertEqual(c.message, 'ok') | |
340 | 103 | Captcha._open = old_open | 141 | finally: |
341 | 104 | 142 | Captcha._open = old_open | |
342 | 105 | @patch_object(Captcha, '_open') | 143 | |
343 | 144 | @patch.object(Captcha, '_open') | ||
344 | 106 | def test_verify_no_captcha_id(self, mock_open): | 145 | def test_verify_no_captcha_id(self, mock_open): |
345 | 107 | c = Captcha(None) | 146 | c = Captcha(None) |
347 | 108 | r = c.verify(None, 'localhost') | 147 | r = c.verify(None, 'localhost', '') |
348 | 109 | 148 | ||
349 | 110 | self.assertFalse(r) | 149 | self.assertFalse(r) |
350 | 111 | self.assertEqual(c.message, 'no-challenge') | 150 | self.assertEqual(c.message, 'no-challenge') |
351 | 112 | 151 | ||
353 | 113 | @patch_object(Captcha, '_open') | 152 | @patch.object(Captcha, '_open') |
354 | 114 | def test_verify_error(self, mock_open): | 153 | def test_verify_error(self, mock_open): |
355 | 115 | c = Captcha(None) | 154 | c = Captcha(None) |
356 | 116 | c.captcha_id = 'challenge-id' | 155 | c.captcha_id = 'challenge-id' |
357 | 117 | 156 | ||
359 | 118 | self.assertRaises(VerifyCaptchaError, c.verify, None, 'localhost') | 157 | self.assertRaises( |
360 | 158 | VerifyCaptchaError, c.verify, None, 'localhost', '' | ||
361 | 159 | ) | ||
362 | 119 | 160 | ||
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 | 7 | import urllib2 | 7 | import urllib2 |
368 | 8 | from urlparse import urlsplit | 8 | from urlparse import urlsplit |
369 | 9 | 9 | ||
371 | 10 | from mock import patch_object, patch | 10 | from mock import patch |
372 | 11 | 11 | ||
373 | 12 | from django.conf import settings | 12 | from django.conf import settings |
374 | 13 | from django.core import mail | 13 | from django.core import mail |
375 | @@ -30,6 +30,7 @@ | |||
376 | 30 | from identityprovider.tests.utils import ( | 30 | from identityprovider.tests.utils import ( |
377 | 31 | LPAccountTestCase, | 31 | LPAccountTestCase, |
378 | 32 | MockHandler, | 32 | MockHandler, |
379 | 33 | patch_settings, | ||
380 | 33 | ) | 34 | ) |
381 | 34 | from identityprovider.utils import is_django_13 | 35 | from identityprovider.utils import is_django_13 |
382 | 35 | 36 | ||
383 | @@ -592,7 +593,7 @@ | |||
384 | 592 | def __init__(self, *args): | 593 | def __init__(self, *args): |
385 | 593 | pass | 594 | pass |
386 | 594 | 595 | ||
388 | 595 | def verify(self, solution, ip_addr): | 596 | def verify(self, solution, ip_addr, email): |
389 | 596 | self.message = 'no-challenge' | 597 | self.message = 'no-challenge' |
390 | 597 | return False | 598 | return False |
391 | 598 | 599 | ||
392 | @@ -635,7 +636,7 @@ | |||
393 | 635 | self.assertEqual(len(mail.outbox), mails_sent) | 636 | self.assertEqual(len(mail.outbox), mails_sent) |
394 | 636 | mail.outbox = [] | 637 | mail.outbox = [] |
395 | 637 | 638 | ||
397 | 638 | @patch_object(logging.Logger, 'debug') | 639 | @patch.object(logging.Logger, 'debug') |
398 | 639 | def test_new_account_when_person_exists_and_account_not(self, mock_debug): | 640 | def test_new_account_when_person_exists_and_account_not(self, mock_debug): |
399 | 640 | person = Person.objects.create(displayname='Person') | 641 | person = Person.objects.create(displayname='Person') |
400 | 641 | EmailAddress.objects.create(email='person@example.com', | 642 | EmailAddress.objects.create(email='person@example.com', |
401 | @@ -815,6 +816,55 @@ | |||
402 | 815 | msg = 'It appears that our captcha service was unable to load' | 816 | msg = 'It appears that our captcha service was unable to load' |
403 | 816 | self.assertContains(r, msg) | 817 | self.assertContains(r, msg) |
404 | 817 | 818 | ||
405 | 819 | def test_new_account_captcha_whitelist(self): | ||
406 | 820 | email = 'canonicaltest@gmail.com' | ||
407 | 821 | pattern = '^canonicaltest(?:\+.+)?@gmail\.com$' | ||
408 | 822 | overrides = dict( | ||
409 | 823 | DISABLE_CAPTCHA_VERIFICATION=False, | ||
410 | 824 | EMAIL_WHITELIST_REGEXP_LIST=[pattern], | ||
411 | 825 | ) | ||
412 | 826 | with patch_settings(**overrides): | ||
413 | 827 | response = _post_new_account( | ||
414 | 828 | self.client, | ||
415 | 829 | email=email | ||
416 | 830 | ) | ||
417 | 831 | # should redirect to '/+email-sent' | ||
418 | 832 | self.assertEqual(response.status_code, 302) | ||
419 | 833 | redirect = response['location'] | ||
420 | 834 | self.assertTrue(redirect.endswith('/+email-sent')) | ||
421 | 835 | |||
422 | 836 | def test_new_account_captcha_whitelist_with_uuid(self): | ||
423 | 837 | email = 'canonicaltest+something@gmail.com' | ||
424 | 838 | pattern = '^canonicaltest(?:\+.+)?@gmail\.com$' | ||
425 | 839 | overrides = dict( | ||
426 | 840 | DISABLE_CAPTCHA_VERIFICATION=False, | ||
427 | 841 | EMAIL_WHITELIST_REGEXP_LIST=[pattern], | ||
428 | 842 | ) | ||
429 | 843 | with patch_settings(**overrides): | ||
430 | 844 | response = _post_new_account( | ||
431 | 845 | self.client, | ||
432 | 846 | email=email | ||
433 | 847 | ) | ||
434 | 848 | # should redirect to '/+email-sent' | ||
435 | 849 | self.assertEqual(response.status_code, 302) | ||
436 | 850 | redirect = response['location'] | ||
437 | 851 | self.assertTrue(redirect.endswith('/+email-sent')) | ||
438 | 852 | |||
439 | 853 | def test_new_account_captcha_whitelist_fail(self): | ||
440 | 854 | email = 'notcanonicaltest@gmail.com' | ||
441 | 855 | pattern = '^canonicaltest(?:\+.+)?@gmail\.com$' | ||
442 | 856 | overrides = dict( | ||
443 | 857 | DISABLE_CAPTCHA_VERIFICATION=False, | ||
444 | 858 | EMAIL_WHITELIST_REGEXP_LIST=[pattern], | ||
445 | 859 | ) | ||
446 | 860 | with patch_settings(**overrides): | ||
447 | 861 | response = _post_new_account( | ||
448 | 862 | self.client, | ||
449 | 863 | email=email | ||
450 | 864 | ) | ||
451 | 865 | msg = 'It appears that our captcha service was unable to load' | ||
452 | 866 | self.assertContains(response, msg) | ||
453 | 867 | |||
454 | 818 | 868 | ||
455 | 819 | class LanguagesTestCase(SSOBaseTestCase): | 869 | class LanguagesTestCase(SSOBaseTestCase): |
456 | 820 | 870 | ||
457 | 821 | 871 | ||
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 | 464 | def _verify_captcha_response(template, request, form): | 464 | def _verify_captcha_response(template, request, form): |
463 | 465 | captcha = Captcha(request.POST.get('recaptcha_challenge_field')) | 465 | captcha = Captcha(request.POST.get('recaptcha_challenge_field')) |
464 | 466 | captcha_solution = request.POST.get('recaptcha_response_field') | 466 | captcha_solution = request.POST.get('recaptcha_response_field') |
465 | 467 | email = request.POST.get('email', '') | ||
466 | 467 | ip_addr = request.environ["REMOTE_ADDR"] | 468 | ip_addr = request.environ["REMOTE_ADDR"] |
467 | 468 | try: | 469 | try: |
469 | 469 | verified = captcha.verify(captcha_solution, ip_addr) | 470 | verified = captcha.verify(captcha_solution, ip_addr, email) |
470 | 470 | if verified: | 471 | if verified: |
471 | 471 | return None | 472 | return None |
472 | 472 | except VerifyCaptchaError, e: | 473 | except VerifyCaptchaError, e: |
473 | 473 | 474 | ||
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 | 6 | NoseDjango==0.8.1 | 6 | NoseDjango==0.8.1 |
479 | 7 | nosexcover | 7 | nosexcover |
480 | 8 | coverage==3.4 | 8 | coverage==3.4 |
482 | 9 | mock==0.6.0 | 9 | mock==0.7.2 |
483 | 10 | wsgiref==0.1.2 | 10 | wsgiref==0.1.2 |
484 | 11 | wsgi-intercept==0.4 | 11 | wsgi-intercept==0.4 |
485 | 12 | zope.testbrowser==3.5.1 | 12 | zope.testbrowser==3.5.1 |
486 | 13 | 13 | ||
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 | 39 | 'NoseDjango==0.8.1', | 39 | 'NoseDjango==0.8.1', |
492 | 40 | 'nosexcover', | 40 | 'nosexcover', |
493 | 41 | 'coverage==3.4', | 41 | 'coverage==3.4', |
495 | 42 | 'mock==0.6.0', | 42 | 'mock==0.7.2', |
496 | 43 | 'wsgiref==0.1.2', | 43 | 'wsgiref==0.1.2', |
497 | 44 | 'wsgi-intercept==0.4', | 44 | 'wsgi-intercept==0.4', |
498 | 45 | 'zope.testbrowser==3.5.1', | 45 | 'zope.testbrowser==3.5.1', |
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_IDENTIFIE R_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 'identityprovid er/models/ captcha. py' r/models/ captcha. py 2011-06-30 04:55:13 +0000 r/models/ captcha. py 2011-07-26 07:31:31 +0000 ror(self. response. traceback) (self, email): EMAIL_WHITELIST _REGEXP_ LIST:
> --- identityprovide
> +++ identityprovide
> @@ -190,3 +194,9 @@
> self._verified = False
> raise VerifyCaptchaEr
> return self._verified
> +
> + def check_whitelist
> + for pattern in settings.
> + 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 :)
> er/schema. py' r/schema. py 2011-07-12 09:41:08 +0000 r/schema. py 2011-07-26 07:31:31 +0000 captcha_ public_ key = StringConfigOpt ion() captcha_ private_ key = StringConfigOpt ion() disable_ captcha_ verification = BoolConfigOption() email_whitelist _regexp_ list = LinesConfigOption( igOption( raw=True)
> === modified file 'identityprovid
> --- identityprovide
> +++ identityprovide
> @@ -78,6 +78,9 @@
> captcha.
> captcha.
> captcha.
> + captcha.
> + item=StringConf
I wasn't sure at first why the raw=True was necessary, until I tried canonicaltest( ?:\+.+) ?@gmail\ .com$') .
repr('^
> + ) er/tests/ test_auth. py' r/tests/ test_auth. py 2011-07-15 15:17:19 +0000 r/tests/ test_auth. py 2011-07-26 07:31:31 +0000
>
> # debug
> debug = ConfigSection()
>
> === modified file 'identityprovid
> --- identityprovide
> +++ identityprovide
Great to fix up all the patch_object's :)
> === modified file 'identityprovid er/tests/ test_captcha. py' r/tests/ test_captcha. py 2011-07-21 16:08:06 +0000 r/tests/ test_captcha. py 2011-07-26 07:31:31 +0000 er/tests/ test_command_ cleanup. py' r/tests/ test_command_ cleanup. py 2011-07-15 11:50:53 +0000 r/tests/ test_command_ cleanup. py 2011-07-26 07:31:31 +0000 er/tests/ test_forms. py' r/tests/ test_forms. py 2011-07-15 11:50:53 +0000 r/tests/ test_forms. py 2011-07-26 07:31:31 +0000 r.api10. forms import WebserviceCreat eAccountForm r.models. openidmodels import OpenIDRPConfig r.tests. utils import patch_settings (form.is_ valid() ) l(form. cleaned_ da...
> --- identityprovide
> +++ identityprovide
> === modified file 'identityprovid
> --- identityprovide
> +++ identityprovide
> === modified file 'identityprovid
> --- identityprovide
> +++ identityprovide
> @@ -18,6 +18,7 @@
> )
> from identityprovide
> from identityprovide
> +from identityprovide
>
> from .utils import SSOBaseTestCase
>
> @@ -114,6 +115,23 @@
> self.assertTrue
> self.assertEqua