Merge lp:~maxiberta/canonical-identity-provider/drop-recaptcha-v1 into lp:canonical-identity-provider/release

Proposed by Maximiliano Bertacchini
Status: Merged
Approved by: Maximiliano Bertacchini
Approved revision: no longer in the source branch.
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~maxiberta/canonical-identity-provider/drop-recaptcha-v1
Merge into: lp:canonical-identity-provider/release
Prerequisite: lp:~maxiberta/canonical-identity-provider/drop-account-registration-captcha
Diff against target: 461 lines (+19/-273)
6 files modified
django_project/settings_base.py (+0/-4)
src/api/v10/tests/utils.py (+0/-6)
src/identityprovider/models/captcha.py (+1/-132)
src/identityprovider/tests/test_models_captcha.py (+12/-128)
src/mockservice/sso_mockserver/wadl.xml (+2/-2)
src/webui/tests/test_views_registration.py (+4/-1)
To merge this branch: bzr merge lp:~maxiberta/canonical-identity-provider/drop-recaptcha-v1
Reviewer Review Type Date Requested Status
Matias Bordese (community) Approve
Review via email: mp+356937@code.launchpad.net

Commit message

Drop the last bits of reCaptcha V1 code.

Description of the change

reCaptcha v1 is dead since March 2018. This is the final branch of reCaptcha v1 cleanup (see prerequisite branch).

To post a comment you must log in.
Revision history for this message
Matias Bordese (matiasb) wrote :

LGTM

review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/settings_base.py'
2--- django_project/settings_base.py 2018-10-19 19:28:43 +0000
3+++ django_project/settings_base.py 2018-10-25 17:56:53 +0000
4@@ -58,13 +58,9 @@
5 CACHE_MIDDLEWARE_ALIAS = 'default'
6 CACHE_MIDDLEWARE_KEY_PREFIX = ''
7 CACHE_MIDDLEWARE_SECONDS = 600
8-CAPTCHA_API_URL = 'http://www.google.com/recaptcha/api'
9-CAPTCHA_API_URL_SECURE = 'https://www.google.com/recaptcha/api'
10-CAPTCHA_IMAGE_URL_PATTERN = 'https://www.google.com/recaptcha/api/image?c=%s'
11 CAPTCHA_PRIVATE_KEY = ''
12 CAPTCHA_PROXIES = {}
13 CAPTCHA_PUBLIC_KEY = ''
14-CAPTCHA_VERIFY_URL = 'http://www.google.com/recaptcha/api/verify'
15 CAPTCHA_V2_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'
16 COMBINE = True
17 COMBO_URL = '/combo/'
18
19=== modified file 'src/api/v10/tests/utils.py'
20--- src/api/v10/tests/utils.py 2018-08-17 18:38:29 +0000
21+++ src/api/v10/tests/utils.py 2018-10-25 17:56:53 +0000
22@@ -43,9 +43,3 @@
23 add_wsgi_intercept(parse.hostname, parse.port, WSGIHandler)
24 self.addCleanup(remove_wsgi_intercept, parse.hostname, parse.port)
25 self.api = ServiceRoot(None, host_url + '/api/1.0')
26-
27- # patch Captcha so it never hits the real network
28- self.mock_captcha_open = self.patch(
29- 'identityprovider.models.captcha.Captcha._open')
30- self.mock_captcha_open.return_value.is_error = False
31- self.mock_captcha_open.return_value.data.return_value = 'true\nyey'
32
33=== modified file 'src/identityprovider/models/captcha.py'
34--- src/identityprovider/models/captcha.py 2018-02-09 20:56:16 +0000
35+++ src/identityprovider/models/captcha.py 2018-10-25 17:56:53 +0000
36@@ -22,14 +22,6 @@
37 self.traceback = traceback
38
39
40-class NewCaptchaError(CaptchaError):
41- """The dummy member is a blank Captcha response."""
42-
43- def __init__(self, traceback, dummy):
44- super(NewCaptchaError, self).__init__(traceback)
45- self.dummy = dummy
46-
47-
48 class VerifyCaptchaError(CaptchaError):
49 def __init__(self, response):
50 super(VerifyCaptchaError, self).__init__(response.traceback)
51@@ -122,122 +114,6 @@
52 return False
53
54
55-class Captcha(CaptchaBase):
56- """Class to capture & abstract interaction with Google reCaptcha v1.
57-
58- .. note: There's no possibility to actually test verification actually
59- returning true, as response, for that you need human interaction.
60-
61- Getting new captcha is quite simple:
62-
63- >>> captcha = Captcha.new()
64- >>> captcha.serialize() #+doctest: ELLIPSIS
65- {'captcha_id': ..., 'image_url': ...}
66-
67- As is verifying received solution:
68-
69- >>> ip_addr = '1.1.1.1'
70- >>> email = 'foo@email.com'
71- >>> captcha = Captcha('captcha-id-received-from-client', email)
72- >>> captcha.verify("this-is-invalid-solution", ip_addr, email)
73- False
74-
75- Once verified solution is cached, so calling again to .verify() method is
76- very cheap (and returns same result):
77-
78- >>> captcha.verify("this-is-invalid-solution", ip_addr, email)
79- False
80-
81- You can also get original response from reCaptcha:
82-
83- >>> print(captcha.response.data())
84- true
85- success
86-
87- """
88-
89- def __init__(self, captcha_id, image_url=None, response=None):
90- assert captcha_id is None or isinstance(captcha_id, basestring), (
91- 'captcha_id should be either None or string: %r' % captcha_id)
92- assert image_url is None or isinstance(image_url, basestring), (
93- 'image_url should be either None or string: %r' % image_url)
94- assert response is None or isinstance(response, CaptchaResponse), (
95- 'response should be either None or a CaptchaResponse: %r' %
96- response)
97- self.captcha_id = captcha_id
98- self.image_url = image_url
99- self.response = response
100- self._verified = None
101- super(Captcha, self).__init__()
102-
103- def serialize(self):
104- return {
105- 'captcha_id': self.captcha_id,
106- 'image_url': self.image_url,
107- }
108-
109- @classmethod
110- def new(cls, timer=None):
111- cls._setup_opener()
112-
113- if not settings.CAPTCHA_PUBLIC_KEY:
114- raise EmptyCaptchaKey(
115- 'Captcha.open: CAPTCHA_PUBLIC_KEY setting is unset.')
116-
117- url = (settings.CAPTCHA_API_URL +
118- '/challenge?k=%s' % settings.CAPTCHA_PUBLIC_KEY)
119- with maybe_contextmanager(timer, 'captcha-new', url):
120- response = cls._open(url)
121- if response.is_error:
122- raise NewCaptchaError(response.traceback, cls(None, None))
123- data = response.data()
124-
125- m = re.search(r"challenge\s*:\s*'(.+?)'", data, re.M | re.S)
126- if m:
127- captcha_id = m.group(1)
128- image_url = settings.CAPTCHA_IMAGE_URL_PATTERN % captcha_id
129- else:
130- captcha_id, image_url = None, None
131- return cls(captcha_id, image_url, response)
132-
133- def verify(self, captcha_solution, remote_ip, email, timer=None):
134- if self._verified is not None:
135- return self._verified
136-
137- if self.check_whitelist(email):
138- return True
139-
140- if not settings.CAPTCHA_PRIVATE_KEY:
141- raise EmptyCaptchaKey(
142- 'Captcha.verify: CAPTCHA_PRIVATE_KEY setting is unset.')
143-
144- if isinstance(captcha_solution, unicode):
145- captcha_solution = captcha_solution.encode('utf-8')
146-
147- request_data = urllib.urlencode({
148- 'privatekey': settings.CAPTCHA_PRIVATE_KEY,
149- 'remoteip': remote_ip,
150- 'challenge': self.captcha_id,
151- 'response': captcha_solution,
152- })
153- request = urllib2.Request(settings.CAPTCHA_VERIFY_URL, request_data)
154- with maybe_contextmanager(timer, 'captcha-verify',
155- settings.CAPTCHA_VERIFY_URL):
156- self.response = self._open(request)
157-
158- if not self.response.is_error:
159- response_data = self.response.data()
160- self.verified, self.message = response_data.split('\n', 1)
161- self._verified = self.verified.lower() == 'true'
162- elif self.captcha_id is None:
163- self.message = 'no-challenge'
164- self._verified = False
165- else:
166- self._verified = False
167- raise VerifyCaptchaError(self.response)
168- return self._verified
169-
170-
171 class CaptchaV2(CaptchaBase):
172 """Class to capture & abstract interaction with Google reCaptcha v2.0."""
173
174@@ -247,13 +123,6 @@
175 with maybe_contextmanager(timer, 'captcha-new', None):
176 super(CaptchaV2, self).__init__()
177
178- @classmethod
179- def new(cls, timer=None):
180- """This method is kept for compatibility with the Captcha v1 class."""
181-
182- cls._setup_opener()
183- return cls(timer)
184-
185 def verify(self, captcha_solution, remote_ip, email, timer=None):
186 if self._verified is not None:
187 return self._verified
188@@ -263,7 +132,7 @@
189
190 if not settings.CAPTCHA_PRIVATE_KEY:
191 raise EmptyCaptchaKey(
192- 'Captcha.verify: CAPTCHA_PRIVATE_KEY setting is unset.')
193+ 'CaptchaV2.verify: CAPTCHA_PRIVATE_KEY setting is unset.')
194
195 if isinstance(captcha_solution, unicode):
196 captcha_solution = captcha_solution.encode('utf-8')
197
198=== modified file 'src/identityprovider/tests/test_models_captcha.py'
199--- src/identityprovider/tests/test_models_captcha.py 2016-03-10 20:24:54 +0000
200+++ src/identityprovider/tests/test_models_captcha.py 2018-10-25 17:56:53 +0000
201@@ -3,19 +3,15 @@
202
203 from contextlib import contextmanager
204 from cStringIO import StringIO
205-from unittest import skip
206-from urllib2 import HTTPError, URLError
207
208 from django.conf import settings
209 from django.test import TestCase
210 from django.test.utils import override_settings
211-from mock import Mock, patch
212+from mock import Mock
213
214 from identityprovider.models.captcha import (
215- Captcha,
216 CaptchaResponse,
217 CaptchaV2,
218- NewCaptchaError,
219 VerifyCaptchaError,
220 )
221 from identityprovider.tests.utils import SSOBaseTestCase
222@@ -39,32 +35,6 @@
223 self.assertEqual(r.data(), 'this is the data')
224
225
226-class CaptchaOpenTestCase(TestCase):
227-
228- def test_open_properly_handles_all_expected_error_codes(self):
229- handled_codes = [111, 113, 408, 500, 502, 503, 504]
230- Captcha._setup_opener()
231- self.addCleanup(setattr, Captcha, 'opener', None)
232-
233- def build_mock_open(code):
234- def mock_open(request):
235- if code < 200:
236- raise URLError([code, "reason"])
237- else:
238- raise HTTPError(
239- None, code, "reason", None, StringIO("test"))
240- return mock_open
241-
242- for code in handled_codes:
243- Captcha.opener.open = build_mock_open(code)
244- response = Captcha._open(None)
245- self.assertTrue(response.is_error)
246- self.assertEqual(response.reason, "reason")
247- self.assertEqual(response.code, code)
248- if code >= 200:
249- self.assertEqual(response.body, "test")
250-
251-
252 @override_settings(CAPTCHA_PUBLIC_KEY='public', CAPTCHA_PRIVATE_KEY='private')
253 class CaptchaBaseTestCase(SSOBaseTestCase):
254
255@@ -78,71 +48,6 @@
256 return_value=self.captcha_response)
257
258
259-class CaptchaTestCase(CaptchaBaseTestCase):
260-
261- def test_opener_is_created_during_init(self):
262- Captcha.opener = None
263- Captcha('test')
264- self.assertTrue(Captcha.opener is not None)
265-
266- @patch('urllib.urlencode')
267- def test_non_ascii_captcha_solution(self, mock_urlencode):
268- captcha_solution = u'\xa3'
269- encoded_solution = captcha_solution.encode('utf-8')
270- mock_urlencode.return_value = 'some text'
271-
272- captcha = Captcha.new()
273- self.mock_captcha_open.return_value.is_error = True
274- captcha.verify(captcha_solution, 'foobar', '')
275-
276- captcha_response = mock_urlencode.call_args[0][0]['response']
277- self.assertEqual(captcha_response, encoded_solution)
278-
279- def test_opener_is_created_when_proxy_is_required(self):
280- Captcha.opener = None
281-
282- with self.settings(CAPTCHA_PROXIES={'http': 'https://foo.com'}):
283- Captcha('test')
284-
285- self.assertTrue(Captcha.opener is not None)
286-
287- def test_serialize(self):
288- captcha = Captcha('id', 'image')
289-
290- self.assertEqual(captcha.serialize(),
291- {'captcha_id': 'id', 'image_url': 'image'})
292-
293- def test_new_captcha_error(self):
294- self.captcha_response.is_error = True
295- with self.assertRaises(NewCaptchaError) as ctx:
296- Captcha.new()
297-
298- e = ctx.exception
299- self.assertEqual(e.dummy.captcha_id, None)
300- self.assertEqual(e.dummy.image_url, None)
301-
302- def test_captcha_open(self):
303- self.mock_captcha_open.side_effect = URLError((-1, 'error'))
304- self.assertRaises(URLError, Captcha.new)
305-
306- def test_new_captcha_uses_timer(self):
307- passed_info = []
308-
309- @contextmanager
310- def timer_fn(category, detail):
311- passed_info.append((category, detail))
312- yield
313-
314- self.captcha_response.code = 200
315-
316- Captcha.new(timer=timer_fn)
317-
318- self.assertEqual(1, len(passed_info))
319- self.assertEqual('captcha-new', passed_info[0][0])
320- self.assertEqual(
321- self.mock_captcha_open.call_args[0][0], passed_info[0][1])
322-
323-
324 class CaptchaV2TestCase(CaptchaBaseTestCase):
325
326 def test_opener_is_created_during_init(self):
327@@ -178,14 +83,10 @@
328 self.assertEqual(settings.CAPTCHA_V2_VERIFY_URL, passed_info[0][1])
329
330
331-class CaptchaVerifyTestCase(CaptchaBaseTestCase):
332-
333- CAPTCHA = Captcha
334- VERIFICATION_URL = settings.CAPTCHA_VERIFY_URL
335- RESPONSE_OK = 'true\nok'
336+class CaptchaV2VerifyTestCase(CaptchaBaseTestCase):
337
338 def test_verify_calls_check_whitelist(self):
339- captcha = self.CAPTCHA(None)
340+ captcha = CaptchaV2(None)
341 captcha.check_whitelist = Mock(return_value=False)
342
343 result = captcha.verify(None, 'localhost', 'foo')
344@@ -195,7 +96,7 @@
345 captcha.check_whitelist.assert_called_with('foo')
346
347 def test_check_whitelist_match(self):
348- captcha = self.CAPTCHA(None)
349+ captcha = CaptchaV2(None)
350
351 regexps = ['not a match', '^foo$']
352 with self.settings(EMAIL_WHITELIST_REGEXP_LIST=regexps):
353@@ -204,7 +105,7 @@
354 self.assertFalse(self.mock_captcha_open.called)
355
356 def test_check_whitelist_no_match(self):
357- captcha = self.CAPTCHA(None)
358+ captcha = CaptchaV2(None)
359
360 regexps = ['not a match', '^bar$']
361 with self.settings(EMAIL_WHITELIST_REGEXP_LIST=regexps):
362@@ -213,28 +114,21 @@
363 self.assertTrue(self.mock_captcha_open.called)
364
365 def test_verify_already_verified(self):
366- c = self.CAPTCHA(None)
367+ c = CaptchaV2(None)
368 c._verified = True
369 r = c.verify(None, None, '')
370 self.assertTrue(r)
371
372 def test_verify_response_ok(self):
373 self.captcha_response.code = 200
374- self.captcha_response.response = StringIO(self.RESPONSE_OK)
375+ self.captcha_response.response = StringIO('{"success": true}')
376
377- c = self.CAPTCHA.new()
378+ c = CaptchaV2()
379 self.assertTrue(c.verify(None, "localhost", ''))
380 self.assertEqual(c.message, 'ok')
381
382- def test_verify_no_captcha_id(self):
383- c = self.CAPTCHA(None)
384-
385- self.captcha_response.is_error = True
386- self.assertFalse(c.verify(None, 'localhost', ''))
387- self.assertEqual(c.message, 'no-challenge')
388-
389 def test_verify_error(self):
390- c = self.CAPTCHA(None)
391+ c = CaptchaV2(None)
392 c.captcha_id = 'challenge-id'
393
394 self.captcha_response.is_error = True
395@@ -247,18 +141,8 @@
396 def timer_fn(category, detail):
397 passed_info.append((category, detail))
398 yield
399- c = self.CAPTCHA(None)
400+ c = CaptchaV2(None)
401 c.verify(None, 'localhost', '', timer=timer_fn)
402 self.assertEqual('captcha-verify', passed_info[0][0])
403- self.assertEqual(self.VERIFICATION_URL, passed_info[0][1])
404-
405-
406-class CaptchaV2VerifyTestCase(CaptchaVerifyTestCase):
407-
408- CAPTCHA = CaptchaV2
409- VERIFICATION_URL = settings.CAPTCHA_V2_VERIFY_URL
410- RESPONSE_OK = '{"success": true}'
411-
412- @skip("This (inherited) test makes no sense for CaptchaV2")
413- def test_verify_no_captcha_id(self):
414- pass
415+ self.assertEqual(
416+ settings.CAPTCHA_V2_VERIFY_URL, passed_info[0][1])
417
418=== modified file 'src/mockservice/sso_mockserver/wadl.xml'
419--- src/mockservice/sso_mockserver/wadl.xml 2012-12-18 18:37:03 +0000
420+++ src/mockservice/sso_mockserver/wadl.xml 2018-10-25 17:56:53 +0000
421@@ -564,7 +564,7 @@
422 required="true" fixed="register">
423 <wadl:doc>The name of the operation being invoked.</wadl:doc>
424 </wadl:param>
425- <wadl:param style="query" required="true"
426+ <wadl:param style="query" required="false"
427 name="captcha_solution">
428 <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
429 Solution for the generated captcha.
430@@ -578,7 +578,7 @@
431 </wadl:doc>
432
433 </wadl:param>
434- <wadl:param style="query" required="true"
435+ <wadl:param style="query" required="false"
436 name="captcha_id">
437 <wadl:doc xmlns="http://www.w3.org/1999/xhtml">
438 ID for the generated captcha
439
440=== modified file 'src/webui/tests/test_views_registration.py'
441--- src/webui/tests/test_views_registration.py 2018-10-25 17:56:53 +0000
442+++ src/webui/tests/test_views_registration.py 2018-10-25 17:56:53 +0000
443@@ -582,6 +582,7 @@
444 self.assertEqual(response.status_code, 405)
445
446
447+@switches(TWOFACTOR=True)
448 class TwoFactorSyncTestCase(SSOBaseTestCase):
449 """Tests for 2fa OTP synchronization."""
450
451@@ -605,7 +606,9 @@
452 email = self.email
453 data = {'email': email, 'password': DEFAULT_USER_PASSWORD}
454 url = reverse('login')
455- return self.client.post(url, data)
456+ response = self.client.post(url, data)
457+ self.assertRedirects(response, reverse('account-index'))
458+ return response
459
460 def test_shows_all_fields_for_synchronization(self):
461 self.factory.make_account(email=self.email)