views: make sms code trigger a standard registration finalization (#69223)

This commit is contained in:
Paul Marillonnet 2022-10-12 11:48:20 +02:00
parent 26f9d2098d
commit 3582bad6b3
3 changed files with 184 additions and 9 deletions

View File

@ -1309,13 +1309,24 @@ class RegistrationCompletionView(CreateView):
request.token = self.token
self.authentication_method = self.token.get('authentication_method', 'email')
self.email = self.token['email']
Lock.lock_email(self.email)
if 'ou' in self.token:
self.ou = OU.objects.get(pk=self.token['ou'])
else:
self.ou = get_default_ou()
self.users = User.objects.filter(email__iexact=self.email).order_by('date_joined')
if self.token.get('email', None):
self.email = self.token['email']
qs_filter = {'email__iexact': self.email}
Lock.lock_email(self.email)
elif self.token.get('phone', None):
self.phone = self.token['phone']
qs_filter = {'phone': self.phone}
Lock.lock_identifier(self.phone)
else:
messages.warning(request, _('Activation failed'))
return utils_misc.redirect(request, 'registration_register')
self.users = User.objects.filter(**qs_filter).order_by('date_joined')
if self.ou:
self.users = self.users.filter(ou=self.ou)
self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
@ -1342,8 +1353,10 @@ class RegistrationCompletionView(CreateView):
if app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT:
help_texts['username'] = app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT
required = list(app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + list(required_fields)
if 'email' in fields:
fields.remove('email')
# identifier fields don't belong here
for field in ('email', 'phone'):
if field in fields:
fields.remove(field)
for field in self.token.get('skip_fields') or []:
if field in fields:
fields.remove(field)
@ -1387,7 +1400,12 @@ class RegistrationCompletionView(CreateView):
else:
ou = get_default_ou()
attributes = {'email': self.email, 'ou': ou}
attributes = {'ou': ou}
if hasattr(self, 'email'):
attributes['email'] = self.email
if hasattr(self, 'phone'):
attributes['phone'] = self.phone
for key in self.token:
if key in app_settings.A2_PRE_REGISTRATION_FIELDS:
attributes[key] = self.token[key]
@ -1411,7 +1429,7 @@ class RegistrationCompletionView(CreateView):
kwargs['instance'] = User.objects.get(id=self.token.get('user_id'))
else:
init_kwargs = {}
for key in ('email', 'first_name', 'last_name', 'ou'):
for key in ('email', 'first_name', 'last_name', 'ou', 'phone'):
if key in attributes:
init_kwargs[key] = attributes[key]
kwargs['instance'] = get_user_model()(**init_kwargs)
@ -1427,7 +1445,10 @@ class RegistrationCompletionView(CreateView):
ctx = super().get_context_data(**kwargs)
ctx['token'] = self.token
ctx['users'] = self.users
ctx['email'] = self.email
if hasattr(self, 'email'):
ctx['email'] = self.email
if hasattr(self, 'phone'):
ctx['phone'] = self.phone
ctx['email_is_unique'] = self.email_is_unique
ctx['create'] = 'create' in self.request.GET
return ctx
@ -1546,6 +1567,7 @@ class RegistrationCompletionView(CreateView):
return utils_misc.redirect(request, self.get_success_url())
def send_registration_success_email(self, user):
# user may not have a registered email by then
if not user.email:
return

View File

@ -46,6 +46,27 @@ ALLOWED_HOSTS = ALLOWED_HOSTS + [ # pylint: disable=used-before-assignment
'cache2.example.com',
]
KNOWN_SERVICES = {
'wcs': {
'default': {
'title': 'test',
'url': 'http://example.org',
'secret': 'chrono',
'orig': 'chrono',
'backoffice-menu-url': 'http://example.org/manage/',
}
},
'passerelle': {
'default': {
'title': 'test',
'url': 'https://foo.whatever.none',
'secret': 'passerelle',
'orig': 'passerelle',
'backoffice-menu-url': 'http://foo.whatever.none/manage/',
}
},
}
A2_AUTH_KERBEROS_ENABLED = False
A2_VALIDATE_EMAIL_DOMAIN = False

View File

@ -14,19 +14,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import date
import json
from datetime import date, timedelta
from unittest import mock
from urllib.parse import urlparse
import requests
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
from django.urls import reverse
from django.utils.http import urlquote
from httmock import HTTMock, remember_called, urlmatch
from authentic2 import models
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.journal.models import Event
from authentic2.forms.profile import modelform_factory
from authentic2.forms.registration import RegistrationCompletionForm
from authentic2.models import SMSCode, Token
from authentic2.utils import misc as utils_misc
from authentic2.validators import EmailValidator
@ -953,3 +957,131 @@ def test_registration_no_identifier(app, db, settings):
resp = app.get(reverse('registration_register'))
resp = resp.form.submit()
assert 'Please provide an email address or a mobile' in resp.text
@urlmatch(netloc='foo.whatever.none')
@remember_called
def sms_service_mock(url, request):
return {
'content': {},
'headers': {
'content-type': 'application/json',
},
'status_code': 200,
}
def test_phone_registration_wrong_code(app, db, settings):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
resp.form.set('registration_code', 'abc')
resp = resp.form.submit()
assert not Token.objects.count()
assert resp.pyquery('li')[0].text_content() == 'Wrong registration code.'
def test_phone_registration_expired_code(app, db, settings, freezer):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('registration_code', code.value)
freezer.move_to(timedelta(hours=1))
resp = resp.form.submit()
assert not Token.objects.count()
assert resp.pyquery('li')[0].text_content() == 'The code has expired.'
def test_phone_registration_cancel(app, db, settings, freezer):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('registration_code', code.value)
resp.form.submit('cancel').follow()
assert not Token.objects.count()
assert not SMSCode.objects.count()
def test_phone_registration_improperly_configured(app, db, settings, freezer, caplog):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = ''
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow().maybe_follow()
assert not Token.objects.count()
assert not SMSCode.objects.count()
assert (
"Something went wrong while trying to send the SMS registration code to you"
in resp.pyquery('li.warning')[0].text_content()
)
assert caplog.records[0].message == 'settings.SMS_URL is not set'
def test_phone_registration_connection_error(app, db, settings, freezer, caplog):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
def mocked_requests_connection_error(*args, **kwargs):
raise requests.ConnectionError('unreachable')
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with mock.patch('authentic2.utils.sms.Requests.send') as mock_send:
mock_send.side_effect = mocked_requests_connection_error
mock_response = mock.Mock(status_code=200)
mock_send.return_value = mock_response
resp = resp.form.submit().follow().maybe_follow()
assert (
"Something went wrong while trying to send the SMS registration code to you"
in resp.pyquery('li.warning')[0].text_content()
)
assert (
caplog.records[0].message
== 'sms registration to +33612345678 using https://foo.whatever.none/ failed: unreachable'
)
def test_phone_registration(app, db, settings):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
code_length = settings.SMS_CODE_LENGTH
assert not SMSCode.objects.count()
assert not Token.objects.count()
resp = app.get(reverse('registration_register'))
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
body = json.loads(sms_service_mock.call['requests'][0].body)
assert body['message'].startswith('Your code is')
code = SMSCode.objects.get()
assert body['message'][-code_length:] == code.value
resp.form.set('registration_code', code.value)
resp = resp.form.submit().follow()
assert Token.objects.count() == 1
resp.form.set('password1', 'Password0')
resp.form.set('password2', 'Password0')
resp.form.set('first_name', 'John')
resp.form.set('last_name', 'Doe')
resp = resp.form.submit().follow()
assert "You have just created an account" in resp.text
user = User.objects.get(first_name='John', last_name='Doe')
assert user.phone == '+33612345678'