Merge lp:~maxiberta/canonical-identity-provider/drop-account-registration-captcha into lp:canonical-identity-provider/release

Proposed by Maximiliano Bertacchini
Status: Superseded
Proposed branch: lp:~maxiberta/canonical-identity-provider/drop-account-registration-captcha
Merge into: lp:canonical-identity-provider/release
Diff against target: 1062 lines (+114/-562)
16 files modified
django_project/settings_base.py (+1/-1)
django_project/settings_devel.py (+0/-1)
src/api/v10/forms.py (+0/-23)
src/api/v10/handlers.py (+18/-21)
src/api/v10/tests/test_forms.py (+0/-24)
src/api/v10/tests/test_handlers.py (+56/-0)
src/api/v20/handlers.py (+5/-42)
src/api/v20/tests/test_handlers.py (+27/-192)
src/api/v20/utils.py (+5/-0)
src/webui/templates/registration/_create_account_form.html (+0/-11)
src/webui/templates/widgets/recaptcha.html (+0/-43)
src/webui/tests/test_views_registration.py (+0/-51)
src/webui/tests/test_views_ui.py (+1/-85)
src/webui/views/registration.py (+0/-26)
src/webui/views/ui.py (+1/-35)
src/webui/views/utils.py (+0/-7)
To merge this branch: bzr merge lp:~maxiberta/canonical-identity-provider/drop-account-registration-captcha
Reviewer Review Type Date Requested Status
Ubuntu One hackers Pending
Review via email: mp+356869@code.launchpad.net

Commit message

Drop captcha from account registration (API & web).

Description of the change

Account registration captcha has been disabled for a long time (years?). And the implementation depends on reCaptcha v1 which is dead since March 2018. So, let's just drop all captcha bits from there.

To post a comment you must log in.

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-11 19:31:10 +0000
3+++ django_project/settings_base.py 2018-10-16 20:29:23 +0000
4@@ -229,7 +229,7 @@
5 'raven.contrib.django.raven_compat',
6 'canonical_raven',
7 ]
8-INTERNAL_IPS = []
9+INTERNAL_IPS = ['127.0.0.1'] # Needed to tell user registration via WEB vs API
10 LANGUAGES = [
11 ['ar', 'Arabic'],
12 ['az', 'Azerbaijani'],
13
14=== modified file 'django_project/settings_devel.py'
15--- django_project/settings_devel.py 2018-02-15 13:17:06 +0000
16+++ django_project/settings_devel.py 2018-10-16 20:29:23 +0000
17@@ -22,7 +22,6 @@
18 'TWOFACTOR': {'is_active': True},
19 'CAN_VIEW_SUPPORT_PHONE': {'is_active': True},
20 'CAPTCHA': {'is_active': False},
21- 'CAPTCHA_NEW_ACCOUNT': {'is_active': False},
22 'PREFLIGHT': {'is_active': True},
23 'LOGIN_BY_TOKEN': {'is_active': True},
24 'SSH_KEY_INTEGRATION': {'is_active': False},
25
26=== modified file 'src/api/v10/forms.py'
27--- src/api/v10/forms.py 2015-05-08 19:25:27 +0000
28+++ src/api/v10/forms.py 2018-10-16 20:29:23 +0000
29@@ -5,17 +5,12 @@
30
31 from __future__ import unicode_literals
32
33-from django import forms
34 from django.forms import fields
35-from django.utils.translation import ugettext_lazy as _
36
37 from identityprovider.forms import NewAccountForm
38-from identityprovider.models.captcha import Captcha
39
40
41 class WebserviceCreateAccountForm(NewAccountForm):
42- captcha_id = fields.CharField(max_length=1024)
43- captcha_solution = fields.CharField(max_length=256)
44 remote_ip = fields.CharField(max_length=256)
45 platform = fields.TypedChoiceField(choices=[
46 ('web', 'Web'), ('desktop', 'Desktop'), ('mobile', 'Mobile')],
47@@ -38,21 +33,3 @@
48 if not validate_redirect_to:
49 validate_redirect_to = None
50 return validate_redirect_to
51-
52- def clean(self):
53- cleaned_data = super(WebserviceCreateAccountForm, self).clean()
54- captcha_id = cleaned_data.get('captcha_id')
55- captcha_solution = cleaned_data.get('captcha_solution')
56-
57- # The remote IP address is absolutely required, and comes from
58- # SSO itself, not from the client. If it's missing, it's a
59- # programming error, and should not be returned to the client
60- # as a validation error. So, we use a normal key lookup here.
61- remote_ip = cleaned_data['remote_ip']
62-
63- captcha = Captcha(captcha_id)
64- email = cleaned_data.get('email', '')
65- if captcha.verify(captcha_solution, remote_ip, email):
66- return cleaned_data
67- # not verified
68- raise forms.ValidationError(_("Wrong captcha solution."))
69
70=== modified file 'src/api/v10/handlers.py'
71--- src/api/v10/handlers.py 2018-08-17 18:38:29 +0000
72+++ src/api/v10/handlers.py 2018-10-16 20:29:23 +0000
73@@ -17,6 +17,7 @@
74 from django.shortcuts import render
75 from django.utils.timezone import now
76 from django.utils.translation import ugettext_lazy as _
77+from gargoyle import gargoyle
78 from oauth_backend.models import Token
79 from piston.emitters import Emitter
80 from piston.handler import BaseHandler
81@@ -38,7 +39,6 @@
82 from identityprovider.models.captcha import (
83 Captcha,
84 NewCaptchaError,
85- VerifyCaptchaError,
86 )
87 from identityprovider.models.const import AuthTokenType, EmailStatus
88 from identityprovider.signals import (
89@@ -184,28 +184,25 @@
90
91 @named_operation
92 def register(self, request):
93+ remote_addr = request.environ['REMOTE_ADDR']
94+ if (not gargoyle.is_active('USER_REGISTRATION_API_ENABLED', request)
95+ and remote_addr not in settings.INTERNAL_IPS):
96+ return api_error(HttpResponseForbidden,
97+ "User registration via API is disabled.")
98+
99 data = request.data
100- data['remote_ip'] = request.environ['REMOTE_ADDR']
101+ data['remote_ip'] = remote_addr
102 form = WebserviceCreateAccountForm(data)
103- try:
104- if not form.is_valid():
105- errors = dict((k, map(unicode, v))
106- for (k, v) in form.errors.items())
107- # XXX: cope with client trimming error messages
108- if unicode(PASSWORD_LEAKED) in errors.get('password', ''):
109- errors['password'] = unicode(_(
110- 'unsafe: leaked by security breach '
111- 'on another website'))
112- result = {'status': 'error', 'errors': errors}
113- return result
114- except VerifyCaptchaError:
115- logger.exception("reCaptcha connection error")
116- msg = unicode(
117- _('Unable to verify captcha. Please try again shortly.'))
118- return {
119- 'status': 'error',
120- 'errors': {'captcha_solution': [msg]}
121- }
122+ if not form.is_valid():
123+ errors = dict((k, map(unicode, v))
124+ for (k, v) in form.errors.items())
125+ # XXX: cope with client trimming error messages
126+ if unicode(PASSWORD_LEAKED) in errors.get('password', ''):
127+ errors['password'] = unicode(_(
128+ 'unsafe: leaked by security breach '
129+ 'on another website'))
130+ result = {'status': 'error', 'errors': errors}
131+ return result
132
133 cleaned_data = form.cleaned_data
134 requested_email = cleaned_data['email']
135
136=== modified file 'src/api/v10/tests/test_forms.py'
137--- src/api/v10/tests/test_forms.py 2015-11-24 14:38:04 +0000
138+++ src/api/v10/tests/test_forms.py 2018-10-16 20:29:23 +0000
139@@ -5,23 +5,12 @@
140
141 from __future__ import unicode_literals
142
143-from django.test.utils import override_settings
144-
145 from api.v10.forms import WebserviceCreateAccountForm
146 from identityprovider.tests.utils import SSOBaseTestCase
147
148
149-@override_settings(CAPTCHA_PUBLIC_KEY='public', CAPTCHA_PRIVATE_KEY='private')
150 class WebServiceCreateAccountFormTestCase(SSOBaseTestCase):
151
152- def setUp(self):
153- super(WebServiceCreateAccountFormTestCase, self).setUp()
154- # patch Captcha so it never hits the real network
155- self.mock_captcha_open = self.patch(
156- 'identityprovider.models.captcha.Captcha._open')
157- self.mock_captcha_open.return_value.is_error = False
158- self.mock_captcha_open.return_value.data.return_value = 'true\nyey'
159-
160 def test_nonascii_password(self):
161 data = {'password': 'Curuzú Cuatiá',
162 'remote_ip': '127.0.0.1'}
163@@ -46,19 +35,6 @@
164 self.assertTrue(form.is_valid())
165 self.assertEqual(form.cleaned_data['platform'], 'desktop')
166
167- def test_captcha_checked_for_whitelist(self):
168- data = {
169- 'email': 'canonicaltest@gmail.com',
170- 'password': 'password1A',
171- 'captcha_id': '1',
172- 'captcha_solution': '2',
173- 'remote_ip': '127.0.0.1',
174- }
175- pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
176- with self.settings(EMAIL_WHITELIST_REGEXP_LIST=[pattern]):
177- form = WebserviceCreateAccountForm(data)
178- self.assertTrue(form.is_valid())
179-
180 def test_default_cleaned_validate_redirect_to(self):
181 data = {
182 'email': 'some@email.com',
183
184=== modified file 'src/api/v10/tests/test_handlers.py'
185--- src/api/v10/tests/test_handlers.py 2018-05-28 17:22:45 +0000
186+++ src/api/v10/tests/test_handlers.py 2018-10-16 20:29:23 +0000
187@@ -391,6 +391,62 @@
188 'captcha_solution': 'foobar',
189 'captcha_id': 'id'})
190
191+ @switches(USER_REGISTRATION_API_ENABLED=False)
192+ @override_settings(INTERNAL_IPS=['127.0.0.1'])
193+ def test_feature_flag_disabled_with_internal_remote_addr(self):
194+ # Request uses 127.0.0.1, which is treated as an internal IP.
195+ email = self.factory.make_email_address()
196+
197+ response = self.api.registrations.register(
198+ email=email, password='MySecretPassword1',
199+ captcha_solution='foobar', captcha_id='id',
200+ displayname='Test User')
201+
202+ self.assertEqual(response['status'], 'ok')
203+ Account.objects.get_by_email(email)
204+
205+ @switches(USER_REGISTRATION_API_ENABLED=False)
206+ @override_settings(INTERNAL_IPS=['10.0.0.1'])
207+ def test_feature_flag_disabled_with_external_remote_addr(self):
208+ # Request uses 127.0.0.1, which is treated as an *external* IP here.
209+ email = self.factory.make_email_address()
210+
211+ with self.assertRaises(HTTPError) as ctx:
212+ self.api.registrations.register(
213+ email=email, password='MySecretPassword1',
214+ captcha_solution='foobar', captcha_id='id',
215+ displayname='Test User')
216+
217+ self.assertEqual(ctx.exception.response.status, 403)
218+
219+ @switches(USER_REGISTRATION_API_ENABLED=True)
220+ @override_settings(INTERNAL_IPS=['127.0.0.1'])
221+ def test_feature_flag_enabled_with_internal_remote_addr(self):
222+ # Request uses 127.0.0.1, which is treated as an internal IP.
223+ email = self.factory.make_email_address()
224+
225+ response = self.api.registrations.register(
226+ email=email, password='MySecretPassword1',
227+ captcha_solution='foobar', captcha_id='id',
228+ displayname='Test User')
229+
230+ self.assertEqual(response['status'], 'ok')
231+ Account.objects.get_by_email(email)
232+
233+ @switches(USER_REGISTRATION_API_ENABLED=True)
234+ @override_settings(INTERNAL_IPS=['10.0.0.1'])
235+ def test_feature_flag_enabled_with_external_remote_addr(self):
236+ # Request uses 127.0.0.1, which is treated as an *external* IP here.
237+ email = self.factory.make_email_address()
238+
239+ response = self.api.registrations.register(
240+ email=email, password='MySecretPassword1',
241+ captcha_solution='foobar', captcha_id='id',
242+ displayname='Test User')
243+
244+ self.assertEqual(response['status'], 'ok')
245+ Account.objects.get_by_email(email)
246+
247
248 class AuthenticationTestCase(SSOBaseTestCase):
249
250
251=== modified file 'src/api/v20/handlers.py'
252--- src/api/v20/handlers.py 2018-08-17 18:38:29 +0000
253+++ src/api/v20/handlers.py 2018-10-16 20:29:23 +0000
254@@ -52,7 +52,6 @@
255 Token,
256 twofactor,
257 )
258-from identityprovider.models.captcha import Captcha, VerifyCaptchaError
259 from identityprovider.models.const import (
260 AccountStatus,
261 AuthLogType,
262@@ -61,7 +60,6 @@
263 )
264 from identityprovider.signals import login_failed, login_succeeded
265 from identityprovider.stats import stats
266-from identityprovider.timeline_helpers import get_request_timing_function
267 from identityprovider.utils import redirection_url_for_token
268 from webservices.launchpad import get_lp_ssh_keys
269
270@@ -282,8 +280,12 @@
271 @throttle()
272 def create(self, request):
273 """Create/register a new account."""
274+ remote_addr = request.environ['REMOTE_ADDR']
275+ if (not gargoyle.is_active('USER_REGISTRATION_API_ENABLED', request)
276+ and remote_addr not in settings.INTERNAL_IPS):
277+ return errors.FEATURE_DISABLED()
278+
279 data = request.data
280-
281 try:
282 email = data['email']
283 password = data['password']
284@@ -294,43 +296,6 @@
285 return errors.INVALID_DATA(**missing)
286
287 username = data.get('username')
288- captcha_required = (gargoyle.is_active('CAPTCHA', request) and
289- gargoyle.is_active('CAPTCHA_NEW_ACCOUNT', request))
290-
291- captcha_id = data.get('captcha_id')
292- captcha_solution = data.get('captcha_solution')
293- if not (captcha_solution and captcha_id) and captcha_required:
294- extra = {}
295- if data.get('create_captcha', True):
296- timer = get_request_timing_function(request)
297- try:
298- extra = Captcha.new(timer=timer).serialize()
299- except Exception:
300- logger.exception('failed to create reCaptcha')
301- return errors.CAPTCHA_REQUIRED(**extra)
302-
303- elif captcha_id:
304- remote_addr = request.environ['REMOTE_ADDR']
305- captcha = Captcha(captcha_id)
306- verified = False
307- try:
308- timer = get_request_timing_function(request)
309- verified = captcha.verify(
310- captcha_solution, remote_addr, data['email'],
311- timer=timer)
312- except VerifyCaptchaError as e:
313- # only expose HTTP error codes to client
314- code = e.response.code if e.response.code > 200 else None
315- return errors.CAPTCHA_ERROR(
316- recaptcha_reason=e.response.reason,
317- recaptcha_status_code=code,
318- recaptcha_body=e.response.body)
319- except Exception:
320- logger.exception("reCaptcha error")
321-
322- if not verified:
323- message = getattr(captcha, 'message', '')
324- return errors.CAPTCHA_FAILURE(captcha_message=message)
325
326 root_url = request.build_absolute_uri('/')
327 try:
328@@ -365,8 +330,6 @@
329 The newly created account will be verified and passwordless,
330 and the owner will not be able to login until a password reset is done.
331
332- Captcha is completely ignored.
333-
334 """
335 data = request.data
336
337
338=== modified file 'src/api/v20/tests/test_handlers.py'
339--- src/api/v20/tests/test_handlers.py 2018-09-13 20:19:21 +0000
340+++ src/api/v20/tests/test_handlers.py 2018-10-16 20:29:23 +0000
341@@ -7,9 +7,8 @@
342 import time
343 import uuid
344
345-from StringIO import StringIO
346 from urllib import quote, urlencode
347-from urlparse import parse_qsl, urlparse, urlunparse
348+from urlparse import urlparse, urlunparse
349
350 from django.conf import settings
351 from django.contrib.auth.hashers import make_password
352@@ -35,7 +34,6 @@
353 AuthToken,
354 EmailAddress,
355 Token,
356- captcha,
357 )
358 from identityprovider.models.const import (
359 AccountCreationRationale,
360@@ -616,195 +614,34 @@
361 self.assert_bad_request(
362 data=self.data, extra={'email': ['Invalid email']})
363
364-
365-@override_settings(CAPTCHA_PUBLIC_KEY='public', CAPTCHA_PRIVATE_KEY='private')
366-class AnonymousAccountRegistrationWithCaptchaHandlerTestCase(BaseTestCase):
367-
368- url = reverse('api-registration')
369-
370- def setUp(self):
371- super(AnonymousAccountRegistrationWithCaptchaHandlerTestCase,
372- self).setUp()
373- p = switches(CAPTCHA=True, CAPTCHA_NEW_ACCOUNT=True)
374- p.patch()
375- self.addCleanup(p.unpatch)
376-
377- self.captcha_response = captcha.CaptchaResponse(
378- code=42, response=StringIO("challenge: '999'"))
379- self.mock_captcha_open = self.patch(
380- 'api.v20.handlers.Captcha._open',
381- return_value=self.captcha_response)
382-
383- self.data = {
384- 'email': self.factory.make_email_address(),
385- 'password': 'asdfASDF1',
386- 'displayname': 'Ricardo the Magnificent',
387- 'captcha_id': '999',
388- 'captcha_solution': 'foo bar',
389- }
390-
391- def test_register_captcha_required(self):
392- captcha_data = {
393- 'captcha_id': '999',
394- 'image_url': settings.CAPTCHA_IMAGE_URL_PATTERN % '999'}
395- del self.data['captcha_id']
396- del self.data['captcha_solution']
397-
398- json_body = self.do_post(self.data, status_code=401)
399-
400- self.assertEqual(json_body['code'], "CAPTCHA_REQUIRED")
401- self.assertIn('A captcha challenge is required', json_body['message'])
402- self.assertIsNone(Account.objects.get_by_email(self.data['email']))
403- self.assertEqual(json_body['extra'], captcha_data)
404-
405- def test_register_captcha_success(self):
406- self.captcha_response.response = StringIO('true\nok')
407+ @switches(USER_REGISTRATION_API_ENABLED=False)
408+ @override_settings(INTERNAL_IPS=['127.0.0.1'])
409+ def test_feature_flag_disabled_with_internal_remote_addr(self):
410+ # Request uses 127.0.0.1, which is treated as an internal IP.
411 json_body = self.do_post(self.data, status_code=201)
412-
413- url_request = self.mock_captcha_open.call_args[0][0]
414- self.assertEqual(
415- url_request.get_full_url(), settings.CAPTCHA_VERIFY_URL)
416- data = dict(parse_qsl(url_request.data))
417- expected = {
418- 'challenge': '999', 'privatekey': settings.CAPTCHA_PRIVATE_KEY,
419- 'remoteip': '127.0.0.1', 'response': 'foo bar'}
420- self.assertEqual(data, expected)
421-
422- self.assertIn('openid', json_body)
423- self.assertIn('href', json_body)
424- self.assertEqual(json_body['email'], self.data['email'])
425- self.assertEqual(json_body['displayname'], self.data['displayname'])
426- self.assertEqual(json_body['status'], 'Active')
427- self.assertEqual(len(json_body['emails']), 1)
428- self.assertIn(quote(self.data['email'], safe='@'),
429- json_body['emails'][0]['href'])
430-
431- def test_register_captcha_failure(self):
432- self.captcha_response.is_error = False
433-
434- self.data['captcha_id'] = '999'
435- self.data['captcha_solution'] = 'foo bar'
436-
437+ self.assert_correct_account_information(json_body)
438+
439+ @switches(USER_REGISTRATION_API_ENABLED=False)
440+ @override_settings(INTERNAL_IPS=['10.0.0.1'])
441+ def test_feature_flag_disabled_with_external_remote_addr(self):
442+ # Request uses 127.0.0.1, which is treated as an *external* IP here.
443 json_body = self.do_post(self.data, status_code=403)
444-
445- url_request = self.mock_captcha_open.call_args[0][0]
446- self.assertEqual(
447- url_request.get_full_url(), settings.CAPTCHA_VERIFY_URL)
448- data = dict(parse_qsl(url_request.data))
449- expected = {
450- 'challenge': '999', 'privatekey': settings.CAPTCHA_PRIVATE_KEY,
451- 'remoteip': '127.0.0.1', 'response': 'foo bar'}
452- self.assertEqual(data, expected)
453-
454- self.assertEqual(json_body['code'], "CAPTCHA_FAILURE")
455- self.assertIn(
456- 'Failed response to captcha challenge.', json_body['message'])
457- self.assertIsNone(Account.objects.get_by_email(self.data['email']))
458-
459- def test_register_captcha_whitelist(self):
460- self.data['email'] = 'canonicaltest@gmail.com'
461- self.data['captcha_id'] = '999'
462- self.data['captcha_solution'] = 'foo bar'
463-
464- with self.settings(**OVERRIDES):
465- self.do_post(self.data, status_code=201)
466-
467- self.assertIsNotNone(Account.objects.get_by_email(self.data['email']))
468- self.assertFalse(self.mock_captcha_open.called)
469-
470- def test_register_captcha_whitelist_with_uuid(self):
471- self.data['email'] = 'canonicaltest+something@gmail.com'
472- self.data['captcha_id'] = '999'
473- self.data['captcha_solution'] = 'foo bar'
474-
475- with self.settings(**OVERRIDES):
476- self.do_post(self.data, status_code=201)
477-
478- self.assertIsNotNone(Account.objects.get_by_email(self.data['email']))
479- self.assertFalse(self.mock_captcha_open.called)
480-
481- def test_register_captcha_whitelist_fail(self):
482- self.data['captcha_id'] = '999'
483- self.data['captcha_solution'] = 'foo bar'
484- self.data['email'] = 'notcanonicaltest@gmail.com'
485-
486- self.captcha_response.is_error = False
487- self.captcha_response.response = StringIO('false\nmessage')
488-
489- with self.settings(**OVERRIDES):
490- self.do_post(self.data, status_code=403)
491-
492- self.assertIsNone(Account.objects.get_by_email(self.data['email']))
493- self.assertTrue(self.mock_captcha_open.called)
494-
495- def test_register_captcha_http_error(self):
496- response = captcha.CaptchaResponse(
497- 500, None, "", "Server error", "Unexpected recaptcha error")
498- error = captcha.VerifyCaptchaError(response)
499- self.mock_captcha_open.side_effect = error
500-
501- self.data['captcha_id'] = '999'
502- self.data['captcha_solution'] = 'foo bar'
503-
504- json_body = self.do_post(self.data, status_code=502)
505-
506- self.assertEqual(json_body['code'], "CAPTCHA_ERROR")
507- self.assertIn('Unable to get a valid response from reCaptcha service',
508- json_body['message'])
509- extra = json_body['extra']
510- self.assertEqual(extra['recaptcha_reason'], "Server error")
511- self.assertEqual(extra['recaptcha_status_code'], 500)
512- self.assertEqual(extra['recaptcha_body'], "Unexpected recaptcha error")
513- self.assertIsNone(Account.objects.get_by_email(self.data['email']))
514-
515- def test_register_captcha_network_error(self):
516- response = captcha.CaptchaResponse(111, None, "", "Connection refused")
517- error = captcha.VerifyCaptchaError(response)
518- self.mock_captcha_open.side_effect = error
519-
520- self.data['captcha_id'] = '999'
521- self.data['captcha_solution'] = 'foo bar'
522-
523- json_body = self.do_post(self.data, status_code=502)
524-
525- self.assertEqual(json_body['code'], "CAPTCHA_ERROR")
526- self.assertIn('Unable to get a valid response from reCaptcha service',
527- json_body['message'])
528- extra = json_body['extra']
529- self.assertEqual(extra['recaptcha_reason'], "Connection refused")
530- self.assertEqual(extra['recaptcha_status_code'], None)
531- self.assertEqual(extra['recaptcha_body'], None)
532- self.assertIsNone(Account.objects.get_by_email(self.data['email']))
533-
534- def test_timeline_for_captcha_generation(self):
535- timeline = Timeline()
536- meta = {
537- 'timeline.timeline': timeline,
538- }
539- data = self.data.copy()
540- data.pop('captcha_id')
541- data.pop('captcha_solution')
542- data['create_captcha'] = True
543-
544- self.do_post(data, status_code=401, **meta)
545-
546- self.assertEqual(1, len(timeline.actions))
547- self.assertEqual('captcha-new', timeline.actions[0].category)
548- self.assertEqual(self.mock_captcha_open.call_args[0][0],
549- timeline.actions[0].detail)
550-
551- def test_timeline_for_captcha_verification(self):
552- self.mock_captcha_open.return_value = captcha.CaptchaResponse(
553- 500, StringIO('false\nsomething'))
554- timeline = Timeline()
555- meta = {
556- 'timeline.timeline': timeline,
557- }
558-
559- self.do_post(self.data, status_code=403, **meta)
560-
561- self.assertEqual(1, len(timeline.actions))
562- self.assertEqual('captcha-verify', timeline.actions[0].category)
563+ self.assertEqual(json_body['code'], "FEATURE_DISABLED")
564+ self.assertEqual("Feature disabled.", json_body['message'])
565+
566+ @switches(USER_REGISTRATION_API_ENABLED=True)
567+ @override_settings(INTERNAL_IPS=['127.0.0.1'])
568+ def test_feature_flag_enabled_with_internal_remote_addr(self):
569+ # Request uses 127.0.0.1, which is treated as an internal IP.
570+ json_body = self.do_post(self.data, status_code=201)
571+ self.assert_correct_account_information(json_body)
572+
573+ @switches(USER_REGISTRATION_API_ENABLED=True)
574+ @override_settings(INTERNAL_IPS=['10.0.0.1'])
575+ def test_feature_flag_enabled_with_external_remote_addr(self):
576+ # Request uses 127.0.0.1, which is treated as an *external* IP here.
577+ json_body = self.do_post(self.data, status_code=201)
578+ self.assert_correct_account_information(json_body)
579
580
581 class AccountRegistrationHandlerTestCase(BaseTestCase):
582@@ -2268,8 +2105,6 @@
583 email = 'foo@foo.com'
584
585 data = {
586- 'captcha_id': 'some-id',
587- 'captcha_solution': 'some solution',
588 'email': 'foo@foo.com',
589 'password': 'some-password-123',
590 'displayname': 'Some User',
591
592=== modified file 'src/api/v20/utils.py'
593--- src/api/v20/utils.py 2018-05-28 17:22:45 +0000
594+++ src/api/v20/utils.py 2018-10-16 20:29:23 +0000
595@@ -67,6 +67,7 @@
596 class ErrorCode:
597 ACCOUNT_NOT_READY = 'account-not-ready'
598 BAD_REQUEST = 'bad-request'
599+ FEATURE_DISABLED = 'feature-disabled'
600 INTERNAL_ERROR = 'internal-server-error'
601 INVALID_DATA = 'invalid-data'
602 INVALID_CREDENTIALS = 'invalid-credentials'
603@@ -141,6 +142,10 @@
604 403, _("Insufficient permissions."),
605 ErrorCode.PERMISSION_REQUIRED),
606
607+ FEATURE_DISABLED=(
608+ 403, _("Feature disabled."),
609+ ErrorCode.FEATURE_DISABLED),
610+
611 RESOURCE_NOT_FOUND=(
612 404, _("The resource requested was not found."),
613 ErrorCode.RESOURCE_NOT_FOUND),
614
615=== modified file 'src/webui/templates/registration/_create_account_form.html'
616--- src/webui/templates/registration/_create_account_form.html 2018-07-04 17:51:43 +0000
617+++ src/webui/templates/registration/_create_account_form.html 2018-10-16 20:29:23 +0000
618@@ -50,17 +50,6 @@
619
620 {% include "widgets/passwords.html" with fields=create_form %}
621
622- {% if captcha_required %}
623- <div class="captcha" id="captcha">
624- {% if captcha_error_message %}
625- <span class="error">
626- {{ captcha_error_message }}
627- </span>
628- {% endif %}
629- {% include "widgets/recaptcha.html" %}
630- </div>
631- {% endif %}
632-
633 <div class="input-row{% if create_form.accept_tos.errors %} haserrors{% endif %} accept-tos-input">
634
635 {% if create_form.accept_tos.errors %}
636
637=== removed file 'src/webui/templates/widgets/recaptcha.html'
638--- src/webui/templates/widgets/recaptcha.html 2014-12-11 17:44:29 +0000
639+++ src/webui/templates/widgets/recaptcha.html 1970-01-01 00:00:00 +0000
640@@ -1,43 +0,0 @@
641-{% comment %}
642-Copyright 2010 Canonical Ltd. This software is licensed under the
643-GNU Affero General Public License version 3 (see the file LICENSE).
644-{% endcomment %}
645-
646-{% load i18n %}
647-{% load static_url %}
648-<script type="text/javascript">
649- var RecaptchaOptions = {
650- theme: 'white',
651- custom_translations: {
652- visual_challenge : "{% trans "Get a visual challenge" %}",
653- audio_challenge : "{% trans "Get an audio challenge" %}",
654- refresh_btn : "{% trans "Get a new challenge" %}",
655- instructions_visual : "{% trans "Type the two words:" %}",
656- instructions_audio : "{% trans "Type what you hear:" %}",
657- help_btn : "{% trans "Help" %}",
658- play_again : "{% trans "Play sound again" %}",
659- cant_hear_this : "{% trans "Download sound as MP3" %}",
660- incorrect_try_again : "{% trans "Incorrect. Try again." %}"
661- },
662- };
663-</script>
664-<div {% if captcha_error %}class='captchaError'{% endif %}>
665-{% ifequal captcha_error "&error=no-challenge" %}
666-<p>
667-{% blocktrans with "support_form"|static_url as support_form_url %}
668-It appears that our captcha service was unable to load on this page.
669-This may be caused by a plugin on your browser.
670-Please correct this and try again. If the problem persists, please <a href="{{ support_form_url }}">contact support</a>
671-{% endblocktrans %}
672-</p>
673-{% endifequal %}
674-<script type="text/javascript" src="{{ CAPTCHA_API_URL_SECURE }}/challenge?k={{ CAPTCHA_PUBLIC_KEY }}{{ captcha_error }}">
675-</script>
676-<noscript>
677- <iframe src="{{ CAPTCHA_API_URL_SECURE }}/noscript?k={{ CAPTCHA_PUBLIC_KEY }}" height="300" width="500" frameborder="0" class="recaptcha-noscript">
678- </iframe>
679- <textarea class="recaptcha-challenge-field" name="recaptcha_challenge_field" rows="3" cols="40">
680- </textarea>
681- <input type="hidden" name="recaptcha_response_field" value="manual_challenge">
682-</noscript>
683-</div>
684
685=== modified file 'src/webui/tests/test_views_registration.py'
686--- src/webui/tests/test_views_registration.py 2018-05-28 20:15:33 +0000
687+++ src/webui/tests/test_views_registration.py 2018-10-16 20:29:23 +0000
688@@ -161,16 +161,6 @@
689 ctx = response.context_data
690 self.assertEqual(ctx['form']['email'].value(), 'test@test.com')
691
692- @switches(CAPTCHA=False)
693- def test_get_optional_captcha_switch_off(self):
694- response = self.get()
695- self.assertEqual(response.context_data['captcha_required'], False)
696-
697- @switches(CAPTCHA=True, CAPTCHA_NEW_ACCOUNT=True)
698- def test_get_optional_captcha_switch_on(self):
699- response = self.get()
700- self.assertEqual(response.context_data['captcha_required'], True)
701-
702 def test_post_required_fields(self):
703 response = self.post()
704 self.assert_form_displayed(
705@@ -213,9 +203,6 @@
706 email=self.TESTDATA['email'],
707 password=self.TESTDATA['password'],
708 displayname=self.TESTDATA['displayname'],
709- captcha_id=None,
710- captcha_solution=None,
711- create_captcha=False,
712 creation_source=WEB_CREATION_SOURCE,
713 )
714
715@@ -242,9 +229,6 @@
716 password=self.TESTDATA['password'],
717 displayname=self.TESTDATA['displayname'],
718 username=self.TESTDATA['username'],
719- captcha_id=None,
720- captcha_solution=None,
721- create_captcha=False,
722 creation_source=WEB_CREATION_SOURCE,
723 )
724 self.TESTDATA.pop('username')
725@@ -337,9 +321,6 @@
726 expected_args = dict(email=self.TESTDATA['email'],
727 password=self.TESTDATA['password'],
728 displayname=self.TESTDATA['displayname'],
729- create_captcha=False,
730- captcha_solution=None,
731- captcha_id=None,
732 creation_source='web-flow',
733 oid_token=token)
734 self.mock_api_register.assert_called_once_with(**expected_args)
735@@ -351,38 +332,6 @@
736 self.assert_form_displayed(response, email=VERIFY_EMAIL_MESSAGE)
737 self.assert_stat_calls(['error.email'])
738
739- def test_post_captcha_required(self):
740- exc = api_errors.CaptchaRequired(Mock())
741- self.mock_api_register.side_effect = exc
742- response = self.post(**self.TESTDATA)
743- self.assert_form_displayed(response)
744- self.assertEqual(response.context_data['captcha_required'], True)
745-
746- def test_post_captcha_failure(self):
747- mock_response = Mock()
748- body = {'extra': {'captcha_message': 'XXX'}}
749- exc = api_errors.CaptchaFailure(mock_response, body)
750- self.mock_api_register.side_effect = exc
751-
752- response = self.post(**self.TESTDATA)
753- self.assert_form_displayed(response)
754- self.assertEqual(response.context_data['captcha_required'], True)
755- self.assertEqual(
756- response.context_data['captcha_error'],
757- '&error=XXX')
758- self.assert_stat_calls(['error.captcha'])
759-
760- def test_post_captcha_error(self):
761- mock_response = Mock()
762- body = {}
763- exc = api_errors.CaptchaError(mock_response, body)
764- self.mock_api_register.side_effect = exc
765-
766- response = self.post(**self.TESTDATA)
767- self.assert_form_displayed(response)
768- self.assertEqual(response.context_data['captcha_required'], True)
769- self.assert_stat_calls(['error.captcha'])
770-
771
772 class RegisterTimelineTestCase(
773 SSOBaseTestCase, RegisterTestMixin, TimelineActionMixin):
774
775=== modified file 'src/webui/tests/test_views_ui.py'
776--- src/webui/tests/test_views_ui.py 2018-09-07 21:54:44 +0000
777+++ src/webui/tests/test_views_ui.py 2018-10-16 20:29:23 +0000
778@@ -10,7 +10,6 @@
779 import urllib2
780 from datetime import date
781 from functools import partial
782-from StringIO import StringIO
783 from urlparse import urlsplit
784
785 from django.conf import settings
786@@ -29,7 +28,6 @@
787 from django.test.utils import override_settings
788 from django.urls import reverse
789 from django.utils.html import escape
790-from gargoyle import gargoyle
791 from gargoyle.testutils import switches
792 from mock import Mock, patch
793 from pyquery import PyQuery
794@@ -50,11 +48,7 @@
795 OpenIDRPConfig,
796 twofactor,
797 )
798-from identityprovider.models.captcha import (
799- Captcha,
800- CaptchaResponse,
801- CaptchaV2,
802-)
803+from identityprovider.models.captcha import CaptchaV2
804 from identityprovider.models.const import (
805 AccountStatus,
806 AuthLogType,
807@@ -65,7 +59,6 @@
808 from identityprovider.tests import DEFAULT_USER_PASSWORD
809 from identityprovider.tests.test_auth import AuthLogTestCaseMixin
810 from identityprovider.tests.utils import (
811- MockHandler,
812 SSOBaseTestCase,
813 TimelineActionMixin,
814 )
815@@ -115,9 +108,6 @@
816 'passwordconfirm': 'Testing123',
817 'accept_tos': True
818 }
819- if gargoyle.is_active('CAPTCHA'):
820- data['recaptcha_challenge_field'] = 'ignored'
821- data['recaptcha_response_field'] = 'ignored'
822
823 return self.client.post(url, data, follow=follow)
824
825@@ -132,24 +122,6 @@
826 assert self.client.login(
827 username=self.data['email'], password=self.data['password'])
828
829- def request_when_captcha_fails(self, url, data):
830- class MockCaptcha(object):
831- def __init__(self, *args):
832- pass
833-
834- def verify(self, solution, ip_addr, email):
835- self.message = 'no-challenge'
836- return False
837-
838- @classmethod
839- def new(cls, env):
840- return cls()
841-
842- with patch.object(ui, 'Captcha', MockCaptcha):
843- r = self.client.post(url, data)
844-
845- return r
846-
847
848 @override_settings(LANGUAGE_CODE='es')
849 class SpanishUIViewsTestCase(BaseTestCase):
850@@ -1348,62 +1320,6 @@
851 self.assertFalse(email.is_verified)
852
853
854-@override_settings(CAPTCHA_PRIVATE_KEY='some-private-key')
855-class CaptchaVerificationTestCase(BaseTestCase):
856-
857- success_status = 302
858-
859- def setUp(self):
860- super(CaptchaVerificationTestCase, self).setUp()
861- mock_handler = MockHandler()
862- mock_handler.set_next_response(200, 'false\nno-challenge')
863- self.patch(Captcha, 'opener', new=urllib2.build_opener(mock_handler))
864-
865- p = switches(CAPTCHA=True)
866- p.patch()
867- self.addCleanup(p.unpatch)
868-
869- def test_new_account_when_form_validation_fails(self):
870- r = self.post_new_account()
871- self.assertTemplateUsed(r, 'registration/new_account.html')
872- msg = 'It appears that our captcha service was unable to load'
873- self.assertContains(r, msg)
874-
875- def test_new_account_captcha_whitelist(self):
876- email = 'canonicaltest@gmail.com'
877- pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
878- with self.settings(EMAIL_WHITELIST_REGEXP_LIST=[pattern]):
879- response = self.post_new_account(email=email)
880- self.assertEqual(response.status_code, self.success_status)
881-
882- def test_new_account_captcha_whitelist_with_uuid(self):
883- email = 'canonicaltest+something@gmail.com'
884- pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
885- with self.settings(EMAIL_WHITELIST_REGEXP_LIST=[pattern]):
886- response = self.post_new_account(email=email)
887- self.assertEqual(response.status_code, self.success_status)
888-
889- def test_new_account_captcha_whitelist_fail(self):
890- email = 'notcanonicaltest@gmail.com'
891- pattern = '^canonicaltest(?:\+.+)?@gmail\.com$'
892- with self.settings(EMAIL_WHITELIST_REGEXP_LIST=[pattern]):
893- response = self.post_new_account(email=email)
894- msg = 'It appears that our captcha service was unable to load'
895- self.assertContains(response, msg)
896-
897- @patch.object(Captcha, '_open')
898- def test_uses_timeline_from_request(self, mock_open):
899- mock_open.return_value = CaptchaResponse(200, StringIO('true\na'))
900- request = Mock()
901- timeline = Timeline()
902- request.META = {'timeline.timeline': timeline}
903- request.POST = {'recaptcha_challenge_field': 'captcha-id'}
904- request.environ = {'REMOTE_ADDR': '127.0.0.1'}
905- ui._verify_captcha_response(None, request, None)
906- self.assertEqual(1, len(timeline.actions))
907- self.assertEqual('captcha-verify', timeline.actions[0].category)
908-
909-
910 class CookiesTestCase(SSOBaseTestCase):
911
912 def setUp(self):
913
914=== modified file 'src/webui/views/registration.py'
915--- src/webui/views/registration.py 2018-05-28 20:15:33 +0000
916+++ src/webui/views/registration.py 2018-10-16 20:29:23 +0000
917@@ -53,7 +53,6 @@
918 requires_cookies,
919 )
920 from webui.views.utils import (
921- add_captcha_settings,
922 display_email_sent,
923 set_session_email,
924 )
925@@ -87,10 +86,6 @@
926 @requires_cookies
927 @require_http_methods(['GET', 'POST'])
928 def new_account(request, token=None):
929- captcha_required = (gargoyle.is_active('CAPTCHA', request) and
930- gargoyle.is_active('CAPTCHA_NEW_ACCOUNT', request))
931- captcha_error = ''
932- captcha_error_message = None
933 rpconfig = get_rpconfig_from_request(request, token)
934
935 def collect_stats(key):
936@@ -108,14 +103,7 @@
937 data = dict((k, v) for k, v in form.cleaned_data.items()
938 if k in ('email', 'password', 'displayname',
939 'username'))
940- data['captcha_id'] = request.POST.get(
941- 'recaptcha_challenge_field'
942- )
943- data['captcha_solution'] = request.POST.get(
944- 'recaptcha_response_field'
945- )
946 # we'll handle our own capture generation
947- data['create_captcha'] = False
948 data['creation_source'] = WEB_CREATION_SOURCE
949 if token:
950 data['oid_token'] = token
951@@ -136,15 +124,6 @@
952 collect_stats('error.email')
953 form._errors['email'] = [VERIFY_EMAIL_MESSAGE]
954
955- except api_errors.CaptchaRequired as e:
956- captcha_required = True
957- collect_stats('captcha_required')
958-
959- except (api_errors.CaptchaFailure, api_errors.CaptchaError) as e:
960- captcha_required = True
961- captcha_error = '&error=' + e.extra.get('captcha_message', '')
962- captcha_error_message = _('Incorrect captcha solution')
963- collect_stats('error.captcha')
964 except Exception as e:
965 return HttpResponseServerError("exception: " + str(e))
966 else:
967@@ -175,12 +154,7 @@
968 'form': form,
969 'rpconfig': rpconfig,
970 'token': token,
971- 'captcha_required': captcha_required,
972- 'captcha_error': captcha_error,
973- 'captcha_error_message': captcha_error_message,
974 }
975- if captcha_required:
976- context = add_captcha_settings(context)
977
978 if form.errors:
979 err = form.errors.get('email', [''])[0]
980
981=== modified file 'src/webui/views/ui.py'
982--- src/webui/views/ui.py 2018-08-24 15:30:53 +0000
983+++ src/webui/views/ui.py 2018-10-16 20:29:23 +0000
984@@ -56,11 +56,7 @@
985
986 )
987 from identityprovider.models import twofactor
988-from identityprovider.models.captcha import (
989- Captcha,
990- CaptchaV2,
991- VerifyCaptchaError
992-)
993+from identityprovider.models.captcha import CaptchaV2
994 from identityprovider.models.const import AccountStatus, AuthTokenType
995 from identityprovider.signals import login_failed, login_succeeded
996 from identityprovider.signed import BadSignedValue
997@@ -94,7 +90,6 @@
998 requires_cookies,
999 )
1000 from webui.views import registration
1001-from webui.views.utils import add_captcha_settings
1002
1003
1004 ACCOUNT_CREATED = _("Your account was created successfully")
1005@@ -150,11 +145,6 @@
1006 self, request, token, rpconfig, form, create_account_form=None):
1007 context = super(LoginView, self).get_context(
1008 request, token=token, rpconfig=rpconfig, form=form)
1009- # add captcha and account creation form
1010- context['captcha_required'] = (
1011- gargoyle.is_active('CAPTCHA', request) and
1012- gargoyle.is_active('CAPTCHA_NEW_ACCOUNT', request))
1013- context = add_captcha_settings(context)
1014 context['create_account_form'] = create_account_form
1015 return context
1016
1017@@ -503,30 +493,6 @@
1018 return registration.new_account(request, token)
1019
1020
1021-def _verify_captcha_response(template, request, form):
1022- captcha = Captcha(request.POST.get('recaptcha_challenge_field'))
1023- captcha_solution = request.POST.get('recaptcha_response_field')
1024- email = request.POST.get('email', '')
1025- ip_addr = request.environ["REMOTE_ADDR"]
1026- try:
1027- timer_fn = get_request_timing_function(request)
1028- verified = captcha.verify(captcha_solution, ip_addr, email,
1029- timer=timer_fn)
1030- if verified:
1031- return None
1032- except VerifyCaptchaError:
1033- logger.exception("reCaptcha connection error")
1034-
1035- # not verified
1036- return render(
1037- request,
1038- template,
1039- add_captcha_settings({
1040- 'form': form,
1041- 'captcha_error': ('&error=%s' % captcha.message),
1042- 'captcha_required': True}))
1043-
1044-
1045 @require_twofactor_authenticated(
1046 _("Please log in to use this confirmation code"))
1047 def confirm_email(request, authtoken, email_address, token=None):
1048
1049=== modified file 'src/webui/views/utils.py'
1050--- src/webui/views/utils.py 2015-05-08 19:25:27 +0000
1051+++ src/webui/views/utils.py 2018-10-16 20:29:23 +0000
1052@@ -43,10 +43,3 @@
1053 def set_session_email(session, email):
1054 """Place information about the current token's email in the session"""
1055 session['token_email'] = email
1056-
1057-
1058-def add_captcha_settings(context):
1059- d = {'CAPTCHA_PUBLIC_KEY': settings.CAPTCHA_PUBLIC_KEY,
1060- 'CAPTCHA_API_URL_SECURE': settings.CAPTCHA_API_URL_SECURE}
1061- d.update(context)
1062- return d