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

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
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+46988@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Rejecting since this is pretty old.

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