Merge lp:~maxiberta/canonical-identity-provider/drop-recaptcha-v1 into lp:canonical-identity-provider/release
- drop-recaptcha-v1
- Merge into trunk
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 |
Related bugs: |
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
Otto Co-Pilot (otto-copilot) wrote : | # |
Running landing tests failed
https:/
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
Running landing tests failed
https:/
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
Running landing tests failed
https:/
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
Running landing tests failed
https:/
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) |
LGTM