Merge lp:~canonical-isd-hackers/canonical-identity-provider/piston_api into lp:canonical-identity-provider/release
- piston_api
- Merge into trunk
Proposed by
David Owen
Status: | Rejected |
---|---|
Rejected by: | Natalia Bidart |
Proposed branch: | lp:~canonical-isd-hackers/canonical-identity-provider/piston_api |
Merge into: | lp:canonical-identity-provider/release |
Diff against target: |
505 lines (+138/-87) 9 files modified
identityprovider/models/captcha.py (+34/-19) identityprovider/tests/test_captcha.py (+8/-8) identityprovider/tests/test_forms.py (+2/-1) identityprovider/tests/test_models_captcha.py (+26/-39) identityprovider/tests/test_views_ui.py (+1/-1) identityprovider/views/ui.py (+20/-11) identityprovider/webservice/forms.py (+22/-6) identityprovider/webservice/models.py (+23/-2) scripts/test (+2/-0) |
To merge this branch: | bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/piston_api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Canonical ISD hackers | Pending | ||
Review via email: mp+46988@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'identityprovider/models/captcha.py' |
2 | --- identityprovider/models/captcha.py 2010-09-24 13:17:26 +0000 |
3 | +++ identityprovider/models/captcha.py 2011-01-20 22:13:02 +0000 |
4 | @@ -11,6 +11,25 @@ |
5 | from django.conf import settings |
6 | |
7 | |
8 | +class CaptchaError(Exception): |
9 | + """All instances of CaptchaError have a traceback member.""" |
10 | + |
11 | + def __init__(self, traceback): |
12 | + self.traceback = traceback |
13 | + |
14 | + |
15 | +class NewCaptchaError(CaptchaError): |
16 | + """The dummy member is a blank Captcha response.""" |
17 | + |
18 | + def __init__(self, traceback, dummy): |
19 | + super(NewCaptchaError, self).__init__(traceback) |
20 | + self.dummy = dummy |
21 | + |
22 | + |
23 | +class VerifyCaptchaError(CaptchaError): |
24 | + pass |
25 | + |
26 | + |
27 | class CaptchaResponse(object): |
28 | """Response returned from _open() in case of error""" |
29 | |
30 | @@ -49,13 +68,13 @@ |
31 | As is verifying received solution: |
32 | |
33 | >>> captcha = Captcha('captcha-id-received-from-client') |
34 | - >>> captcha.verify("this-is-invalid-solution") |
35 | + >>> captcha.verify("this-is-invalid-solution", ip_addr) |
36 | False |
37 | |
38 | Once verified solution is cached, so calling again to .verify() method is |
39 | very cheap (and returns same result): |
40 | |
41 | - >>> captcha.verify("this-is-invalid-solution") |
42 | + >>> captcha.verify("this-is-invalid-solution", ip_addr) |
43 | False |
44 | |
45 | You can also get original response from reCaptcha: |
46 | @@ -68,8 +87,10 @@ |
47 | |
48 | opener = None |
49 | |
50 | - def __init__(self, env, captcha_id, image_url=None, response=None): |
51 | - self.env = env |
52 | + def __init__(self, captcha_id, image_url=None, response=None): |
53 | + assert captcha_id is None or isinstance(captcha_id, basestring) |
54 | + assert image_url is None or isinstance(image_url, basestring) |
55 | + assert response is None or isinstance(response, CaptchaResponse) |
56 | self.captcha_id = captcha_id |
57 | self.image_url = image_url |
58 | self.response = response |
59 | @@ -96,16 +117,13 @@ |
60 | } |
61 | |
62 | @classmethod |
63 | - def new(cls, env): |
64 | + def new(cls): |
65 | cls._setup_opener() |
66 | url = (settings.CAPTCHA_API_URL + |
67 | '/challenge?k=%s' % settings.CAPTCHA_PUBLIC_KEY) |
68 | - response = cls._open(url, env) |
69 | + response = cls._open(url) |
70 | if response.is_error: |
71 | - env['oops-dump'] = True |
72 | - logging.warning(response.traceback) |
73 | - logging.warning("Failed to connect to reCaptcha server") |
74 | - return cls(env, None, None) |
75 | + raise NewCaptchaError(response.traceback, cls(None, None)) |
76 | |
77 | data = response.data() |
78 | m = re.search(r"challenge\s*:\s*'(.+?)'", data, re.M | re.S) |
79 | @@ -114,10 +132,10 @@ |
80 | image_url = settings.CAPTCHA_IMAGE_URL_PATTERN % captcha_id |
81 | else: |
82 | captcha_id, image_url = None, None |
83 | - return cls(env, captcha_id, image_url, response) |
84 | + return cls(captcha_id, image_url, response) |
85 | |
86 | @classmethod |
87 | - def _open(cls, request, env): |
88 | + def _open(cls, request): |
89 | default_timeout = socket.getdefaulttimeout() |
90 | socket.setdefaulttimeout(getattr(settings, 'CAPTCHA_TIMEOUT', 10)) |
91 | |
92 | @@ -142,7 +160,7 @@ |
93 | |
94 | return CaptchaResponse(response.code, response, None) |
95 | |
96 | - def verify(self, captcha_solution): |
97 | + def verify(self, captcha_solution, remote_ip): |
98 | if self._verified is not None: |
99 | return self._verified |
100 | |
101 | @@ -155,12 +173,12 @@ |
102 | |
103 | request_data = urllib.urlencode({ |
104 | 'privatekey': settings.CAPTCHA_PRIVATE_KEY, |
105 | - 'remoteip': self.env['REMOTE_ADDR'], |
106 | + 'remoteip': remote_ip, |
107 | 'challenge': self.captcha_id, |
108 | 'response': captcha_solution, |
109 | }) |
110 | request = urllib2.Request(settings.CAPTCHA_VERIFY_URL, request_data) |
111 | - self.response = self._open(request, self.env) |
112 | + self.response = self._open(request) |
113 | |
114 | if not self.response.is_error: |
115 | response_data = self.response.data() |
116 | @@ -171,8 +189,5 @@ |
117 | self._verfied = False |
118 | else: |
119 | self._verified = False |
120 | - if getattr(self.env, '__setitem__', False): |
121 | - self.env['oops-dump'] = True |
122 | - logging.warning(self.response.traceback) |
123 | - logging.warning("reCaptcha connection error") |
124 | + raise VerifyCaptchaError(self.response.traceback) |
125 | return self._verified |
126 | |
127 | === modified file 'identityprovider/tests/test_admin.py' |
128 | === modified file 'identityprovider/tests/test_captcha.py' |
129 | --- identityprovider/tests/test_captcha.py 2010-11-03 16:06:08 +0000 |
130 | +++ identityprovider/tests/test_captcha.py 2011-01-20 22:13:02 +0000 |
131 | @@ -26,7 +26,7 @@ |
132 | def test_opener_is_created_during_init(self): |
133 | Captcha.opener = None |
134 | |
135 | - captcha = Captcha({}, 'test') |
136 | + captcha = Captcha('test') |
137 | |
138 | self.assertTrue(Captcha.opener is not None) |
139 | |
140 | @@ -41,10 +41,10 @@ |
141 | encoded_solution = captcha_solution.encode('utf-8') |
142 | mock_urlencode.return_value = 'some text' |
143 | |
144 | - captcha = Captcha({'REMOTE_ADDR': 'foobar'}, None) |
145 | + captcha = Captcha(None) |
146 | captcha._open = Mock() |
147 | captcha._open.return_value.is_error = True |
148 | - captcha.verify(captcha_solution) |
149 | + captcha.verify(captcha_solution, 'foobar') |
150 | |
151 | captcha_response = mock_urlencode.call_args[0][0]['response'] |
152 | self.assertEqual(captcha_response, encoded_solution) |
153 | @@ -56,7 +56,7 @@ |
154 | settings.CAPTCHA_USE_PROXY = True |
155 | settings.CAPTCHA_PROXIES = {} |
156 | |
157 | - captcha = Captcha({}, 'test') |
158 | + captcha = Captcha('test') |
159 | |
160 | self.assertTrue(Captcha.opener is not None) |
161 | |
162 | @@ -73,20 +73,20 @@ |
163 | Captcha.opener.open = mock_open |
164 | |
165 | while handled_codes: |
166 | - response = Captcha._open(None, {}) |
167 | + response = Captcha._open(None) |
168 | self.assertTrue(response.is_error) |
169 | |
170 | def test_verify_is_short_circuited_when_disabled_in_settings(self): |
171 | settings.DISABLE_CAPTCHA_VERIFICATION = True |
172 | |
173 | - captcha = Captcha({'REMOTE_ADDR': '127.0.0.1'}, 'test') |
174 | - verify_result = captcha.verify('solution') |
175 | + captcha = Captcha('test') |
176 | + verify_result = captcha.verify('solution', '127.0.0.1') |
177 | |
178 | self.assertTrue(verify_result) |
179 | self.assertTrue(captcha.response is None) |
180 | |
181 | def test_serialize(self): |
182 | - captcha = Captcha({}, 'id', 'image') |
183 | + captcha = Captcha('id', 'image') |
184 | |
185 | self.assertEquals(captcha.serialize(), |
186 | {'captcha_id': 'id', 'image_url': 'image'}) |
187 | |
188 | === modified file 'identityprovider/tests/test_forms.py' |
189 | --- identityprovider/tests/test_forms.py 2010-07-05 16:15:30 +0000 |
190 | +++ identityprovider/tests/test_forms.py 2011-01-20 22:13:02 +0000 |
191 | @@ -84,7 +84,8 @@ |
192 | forms.get_current_browser_request = self.old_get_request |
193 | |
194 | def test_nonascii_password(self): |
195 | - data = {'password': 'Curuzú Cuatiá'} |
196 | + data = {'password': 'Curuzú Cuatiá', |
197 | + 'remote_ip': '127.0.0.1'} |
198 | form = WebserviceCreateAccountForm(data=data) |
199 | self.assertFalse(form.is_valid()) |
200 | self.assertEqual(form.errors['password'][0], |
201 | |
202 | === modified file 'identityprovider/tests/test_models_captcha.py' |
203 | --- identityprovider/tests/test_models_captcha.py 2010-10-25 20:17:15 +0000 |
204 | +++ identityprovider/tests/test_models_captcha.py 2011-01-20 22:13:02 +0000 |
205 | @@ -9,7 +9,12 @@ |
206 | from cStringIO import StringIO |
207 | from django.conf import settings |
208 | |
209 | -from identityprovider.models.captcha import Captcha, CaptchaResponse |
210 | +from identityprovider.models.captcha import ( |
211 | + Captcha, |
212 | + CaptchaResponse, |
213 | + NewCaptchaError, |
214 | + VerifyCaptchaError, |
215 | +) |
216 | |
217 | |
218 | class CaptchaResponseTestCase(unittest.TestCase): |
219 | @@ -33,15 +38,16 @@ |
220 | class CaptchaTestCase(unittest.TestCase): |
221 | |
222 | @patch_object(Captcha, '_open') |
223 | - def test_new_captcha_oops(self, mock_open): |
224 | - c = Captcha.new({}) |
225 | - self.assertTrue(c.env['oops-dump']) |
226 | + def test_new_captcha_error(self, mock_open): |
227 | + self.assertRaises(NewCaptchaError, Captcha.new) |
228 | |
229 | @patch_object(Captcha, '_open') |
230 | def test_new_captcha_no_challenge(self, mock_open): |
231 | - c = Captcha.new({}) |
232 | - self.assertEqual(c.captcha_id, None) |
233 | - self.assertEqual(c.image_url, None) |
234 | + try: |
235 | + Captcha.new() |
236 | + except NewCaptchaError, e: |
237 | + self.assertEqual(e.dummy.captcha_id, None) |
238 | + self.assertEqual(e.dummy.image_url, None) |
239 | |
240 | def test_captcha_open(self): |
241 | class MockOpener(object): |
242 | @@ -50,7 +56,7 @@ |
243 | old_opener = Captcha.opener |
244 | Captcha.opener = MockOpener() |
245 | |
246 | - self.assertRaises(urllib2.URLError, Captcha.new, {}) |
247 | + self.assertRaises(urllib2.URLError, Captcha.new) |
248 | |
249 | Captcha.opener = old_opener |
250 | |
251 | @@ -66,30 +72,30 @@ |
252 | |
253 | @patch_object(Captcha, '_open') |
254 | def test_verify_already_verified(self, mock_open): |
255 | - c = Captcha.new({}) |
256 | + c = Captcha(None) |
257 | c._verified = True |
258 | - r = c.verify(None) |
259 | + r = c.verify(None, None) |
260 | self.assertTrue(r) |
261 | |
262 | @patch_object(Captcha, '_open') |
263 | def test_verify_no_verification(self, mock_open): |
264 | settings.DISABLE_CAPTCHA_VERIFICATION = True |
265 | |
266 | - c = Captcha.new({}) |
267 | - r = c.verify(None) |
268 | + c = Captcha(None) |
269 | + r = c.verify(None, None) |
270 | self.assertTrue(r) |
271 | |
272 | @patch_object(Captcha, '_open') |
273 | def test_verify_response_ok(self, mock_open): |
274 | @classmethod |
275 | - def mock_open(cls, request, env): |
276 | + def mock_open(cls, request): |
277 | r = CaptchaResponse(200, StringIO('true\nok')) |
278 | return r |
279 | old_open = Captcha._open |
280 | Captcha._open = mock_open |
281 | |
282 | - c = Captcha.new({'REMOTE_ADDR': 'localhost'}) |
283 | - r = c.verify(None) |
284 | + c = Captcha.new() |
285 | + r = c.verify(None, "localhost") |
286 | |
287 | self.assertTrue(r) |
288 | self.assertEqual(c.message, 'ok') |
289 | @@ -98,34 +104,15 @@ |
290 | |
291 | @patch_object(Captcha, '_open') |
292 | def test_verify_no_captcha_id(self, mock_open): |
293 | - c = Captcha.new({'REMOTE_ADDR': 'localhost'}) |
294 | - r = c.verify(None) |
295 | + c = Captcha(None) |
296 | + r = c.verify(None, 'localhost') |
297 | |
298 | self.assertFalse(r) |
299 | self.assertEqual(c.message, 'no-challenge') |
300 | |
301 | @patch_object(Captcha, '_open') |
302 | def test_verify_error(self, mock_open): |
303 | - class MockEnv(dict): |
304 | - def __getattribute__(self, attr): |
305 | - if attr == '__setitem__': |
306 | - raise AttributeError() |
307 | - else: |
308 | - return super(MockEnv, self).__getattribute__(attr) |
309 | - |
310 | - env = MockEnv({'REMOTE_ADDR': 'localhost'}) |
311 | - c = Captcha(env, 'challenge-id') |
312 | - r = c.verify(None) |
313 | - |
314 | - self.assertFalse(r) |
315 | - self.assertFalse('oops-dump' in c.env) |
316 | - |
317 | - @patch_object(Captcha, '_open') |
318 | - def test_verify_error_oops(self, mock_open): |
319 | - c = Captcha.new({'REMOTE_ADDR': 'localhost'}) |
320 | + c = Captcha(None) |
321 | c.captcha_id = 'challenge-id' |
322 | - r = c.verify(None) |
323 | - |
324 | - self.assertFalse(r) |
325 | - self.assertTrue(c.env['oops-dump']) |
326 | - |
327 | + |
328 | + self.assertRaises(VerifyCaptchaError, c.verify, None, 'localhost') |
329 | |
330 | === modified file 'identityprovider/tests/test_views_ui.py' |
331 | --- identityprovider/tests/test_views_ui.py 2010-11-11 13:07:16 +0000 |
332 | +++ identityprovider/tests/test_views_ui.py 2011-01-20 22:13:02 +0000 |
333 | @@ -565,7 +565,7 @@ |
334 | def __init__(self, *args): |
335 | pass |
336 | |
337 | - def verify(self, solution): |
338 | + def verify(self, solution, ip_addr): |
339 | self.message = 'no-challenge' |
340 | return False |
341 | |
342 | |
343 | === modified file 'identityprovider/views/ui.py' |
344 | --- identityprovider/views/ui.py 2010-11-16 19:36:42 +0000 |
345 | +++ identityprovider/views/ui.py 2011-01-20 22:13:02 +0000 |
346 | @@ -29,7 +29,7 @@ |
347 | TokenForm) |
348 | from identityprovider.models import (Account, AccountPassword, AuthToken, |
349 | AuthTokenFactory, EmailAddress, get_type_of_token, verify_token_string) |
350 | -from identityprovider.models.captcha import Captcha |
351 | +from identityprovider.models.captcha import Captcha, VerifyCaptchaError |
352 | from identityprovider.models.const import (AccountStatus, EmailStatus, |
353 | LoginTokenType) |
354 | from identityprovider.models.openidmodels import OpenIDRPConfig |
355 | @@ -421,17 +421,26 @@ |
356 | |
357 | |
358 | def _verify_captcha_response(template, request, form): |
359 | - captcha = Captcha(request.environ, |
360 | - request.POST.get('recaptcha_challenge_field')) |
361 | + captcha = Captcha(request.POST.get('recaptcha_challenge_field')) |
362 | captcha_solution = request.POST.get('recaptcha_response_field') |
363 | - verified = captcha.verify(captcha_solution) |
364 | - if not verified: |
365 | - return render_to_response(template, |
366 | - RequestContext(request, {'form': form, |
367 | - 'CAPTCHA_PUBLIC_KEY': settings.CAPTCHA_PUBLIC_KEY, |
368 | - 'captcha_error': ('&error=%s' % captcha.message), |
369 | - })) |
370 | - return None |
371 | + ip_addr = request.environ["REMOTE_ADDR"] |
372 | + try: |
373 | + verified = captcha.verify(captcha_solution, ip_addr) |
374 | + if verified: |
375 | + return None |
376 | + except VerifyCaptchaError, e: |
377 | + if getattr(request.environ, '__setitem__', False): |
378 | + request.environ['oops-dump'] = True |
379 | + logging.warning(e.traceback) |
380 | + logging.warning("reCaptcha connection error") |
381 | + |
382 | + # not verified |
383 | + return render_to_response( |
384 | + template, |
385 | + RequestContext(request, { |
386 | + 'form': form, |
387 | + 'CAPTCHA_PUBLIC_KEY': settings.CAPTCHA_PUBLIC_KEY, |
388 | + 'captcha_error': ('&error=%s' % captcha.message)})) |
389 | |
390 | |
391 | # {{workflow}} |
392 | |
393 | === modified file 'identityprovider/webservice/forms.py' |
394 | --- identityprovider/webservice/forms.py 2010-08-23 14:51:12 +0000 |
395 | +++ identityprovider/webservice/forms.py 2011-01-20 22:13:02 +0000 |
396 | @@ -9,7 +9,7 @@ |
397 | from lazr.restful.utils import get_current_browser_request |
398 | |
399 | from identityprovider.utils import password_policy_compliant |
400 | -from identityprovider.models.captcha import Captcha |
401 | +from identityprovider.models.captcha import Captcha, VerifyCaptchaError |
402 | |
403 | |
404 | PASSWORD_POLICY_ERROR = _("Password must be at least " |
405 | @@ -22,6 +22,7 @@ |
406 | password = fields.CharField(max_length=256) |
407 | captcha_id = fields.CharField(max_length=1024) |
408 | captcha_solution = fields.CharField(max_length=256) |
409 | + remote_ip = fields.CharField(max_length=256) |
410 | |
411 | def clean_password(self): |
412 | if 'password' in self.cleaned_data: |
413 | @@ -40,9 +41,24 @@ |
414 | captcha_id = cleaned_data.get('captcha_id') |
415 | captcha_solution = cleaned_data.get('captcha_solution') |
416 | |
417 | + # The remote IP address is absolutely required, and comes from |
418 | + # SSO itself, not from the client. If it's missing, it's a |
419 | + # programming error, and should not be returned to the client |
420 | + # as a validation error. So, we use a normal key lookup here. |
421 | + remote_ip = cleaned_data['remote_ip'] |
422 | + |
423 | request = get_current_browser_request() |
424 | - captcha = Captcha(request.environment, captcha_id) |
425 | - if not captcha.verify(captcha_solution): |
426 | - raise forms.ValidationError(_("Wrong captcha solution.")) |
427 | - |
428 | - return cleaned_data |
429 | + |
430 | + captcha = Captcha(captcha_id) |
431 | + |
432 | + try: |
433 | + if captcha.verify(captcha_solution, remote_ip): |
434 | + return cleaned_data |
435 | + except VerifyCaptchaError, e: |
436 | + if getattr(request.environment, '__setitem__', False): |
437 | + request.environment['oops-dump'] = True |
438 | + logging.warning(e.traceback) |
439 | + logging.warning("reCaptcha connection error") |
440 | + |
441 | + # not verified |
442 | + raise forms.ValidationError(_("Wrong captcha solution.")) |
443 | |
444 | === modified file 'identityprovider/webservice/models.py' |
445 | --- identityprovider/webservice/models.py 2011-01-11 15:50:14 +0000 |
446 | +++ identityprovider/webservice/models.py 2011-01-20 22:13:02 +0000 |
447 | @@ -26,7 +26,10 @@ |
448 | IAuthenticationSet, IRegistrationSet, ICaptchaSet) |
449 | from identityprovider.webservice.forms import ( |
450 | WebserviceCreateAccountForm, PASSWORD_POLICY_ERROR) |
451 | -from identityprovider.models.captcha import Captcha as CaptchaModel |
452 | +from identityprovider.models.captcha import ( |
453 | + Captcha as CaptchaModel, |
454 | + NewCaptchaError, |
455 | +) |
456 | from identityprovider.views.server import get_team_memberships |
457 | from identityprovider.utils import (CannotResetPasswordException, |
458 | PersonAndAccountNotFoundException, get_person_and_account_by_email) |
459 | @@ -225,6 +228,16 @@ |
460 | class RegistrationSet(make_set('BaseRegistrationSet', 'registration', |
461 | IRegistrationSet, Account, 'id')): |
462 | def register(self, **kwargs): |
463 | + request = get_current_browser_request() |
464 | + # TODO: request doesn't exist in a test, unless we go through |
465 | + # the test client browser. |
466 | + if request is None: |
467 | + kwargs['remote_ip'] = '127.0.0.1' |
468 | + else: |
469 | + if hasattr(request, 'environment'): |
470 | + kwargs['remote_ip'] = request.environment['REMOTE_ADDR'] |
471 | + else: |
472 | + kwargs['remote_ip'] = request.environ['REMOTE_ADDR'] |
473 | form = WebserviceCreateAccountForm(kwargs) |
474 | if not form.is_valid(): |
475 | errors = dict((k, map(unicode, v)) |
476 | @@ -341,7 +354,15 @@ |
477 | Captcha, 'id')): |
478 | def new(self): |
479 | request = get_current_browser_request() |
480 | - return CaptchaModel.new(request.environment).serialize() |
481 | + try: |
482 | + return CaptchaModel.new().serialize() |
483 | + except NewCaptchaError, e: |
484 | + request.environment['oops-dump'] = True |
485 | + logging.warning(e.traceback) |
486 | + logging.warning("Failed to connect to reCaptcha server") |
487 | + # TODO: Some better return here? |
488 | + return e.dummy |
489 | + |
490 | |
491 | |
492 | class SSOWebServiceRootResource(RootResource): |
493 | |
494 | === modified file 'scripts/test' |
495 | --- scripts/test 2010-12-16 20:59:34 +0000 |
496 | +++ scripts/test 2011-01-20 22:13:02 +0000 |
497 | @@ -144,6 +144,8 @@ |
498 | # Creating nice (HTML) coverage report |
499 | rm -rf $PROJECT_DIR/coverage |
500 | |
501 | +exit |
502 | + |
503 | # coverage found in Lucid won't create the output directory on its |
504 | # own. |
505 | mkdir $PROJECT_DIR/coverage |
Rejecting since this is pretty old.