Projet

Général

Profil

0010-views-make-sms-code-trigger-a-standard-registration-.patch

Paul Marillonnet, 13 octobre 2022 14:06

Télécharger (11,3 ko)

Voir les différences:

Subject: [PATCH 10/10] views: make sms code trigger a standard registration
 finalization (#69223)

 src/authentic2/views.py    |  38 ++++++++---
 tests/settings.py          |  21 ++++++
 tests/test_registration.py | 135 ++++++++++++++++++++++++++++++++++++-
 3 files changed, 185 insertions(+), 9 deletions(-)
src/authentic2/views.py
1307 1307
        request.token = self.token
1308 1308

  
1309 1309
        self.authentication_method = self.token.get('authentication_method', 'email')
1310
        self.email = self.token['email']
1311
        Lock.lock_email(self.email)
1312 1310
        if 'ou' in self.token:
1313 1311
            self.ou = OU.objects.get(pk=self.token['ou'])
1314 1312
        else:
1315 1313
            self.ou = get_default_ou()
1316
        self.users = User.objects.filter(email__iexact=self.email).order_by('date_joined')
1314

  
1315
        if self.token.get('email', None):
1316
            self.email = self.token['email']
1317
            qs_filter = {'email__iexact': self.email}
1318
            Lock.lock_email(self.email)
1319
        elif self.token.get('phone', None):
1320
            self.phone = self.token['phone']
1321
            qs_filter = {'phone': self.phone}
1322
            Lock.lock_identifier(self.phone)
1323
        else:
1324
            messages.warning(request, _('Activation failed'))
1325
            return utils_misc.redirect(request, 'registration_register')
1326

  
1327
        self.users = User.objects.filter(**qs_filter).order_by('date_joined')
1317 1328
        if self.ou:
1318 1329
            self.users = self.users.filter(ou=self.ou)
1319 1330
        self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
......
1340 1351
        if app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT:
1341 1352
            help_texts['username'] = app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT
1342 1353
        required = list(app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + list(required_fields)
1343
        if 'email' in fields:
1344
            fields.remove('email')
1354
        # identifier fields don't belong here
1355
        for field in ('email', 'phone'):
1356
            if field in fields:
1357
                fields.remove(field)
1345 1358
        for field in self.token.get('skip_fields') or []:
1346 1359
            if field in fields:
1347 1360
                fields.remove(field)
......
1385 1398
        else:
1386 1399
            ou = get_default_ou()
1387 1400

  
1388
        attributes = {'email': self.email, 'ou': ou}
1401
        attributes = {'ou': ou}
1402
        if hasattr(self, 'email'):
1403
            attributes['email'] = self.email
1404
        if hasattr(self, 'phone'):
1405
            attributes['phone'] = self.phone
1406

  
1389 1407
        for key in self.token:
1390 1408
            if key in app_settings.A2_PRE_REGISTRATION_FIELDS:
1391 1409
                attributes[key] = self.token[key]
......
1409 1427
            kwargs['instance'] = User.objects.get(id=self.token.get('user_id'))
1410 1428
        else:
1411 1429
            init_kwargs = {}
1412
            for key in ('email', 'first_name', 'last_name', 'ou'):
1430
            for key in ('email', 'first_name', 'last_name', 'ou', 'phone'):
1413 1431
                if key in attributes:
1414 1432
                    init_kwargs[key] = attributes[key]
1415 1433
            kwargs['instance'] = get_user_model()(**init_kwargs)
......
1425 1443
        ctx = super().get_context_data(**kwargs)
1426 1444
        ctx['token'] = self.token
1427 1445
        ctx['users'] = self.users
1428
        ctx['email'] = self.email
1446
        if hasattr(self, 'email'):
1447
            ctx['email'] = self.email
1448
        if hasattr(self, 'phone'):
1449
            ctx['phone'] = self.phone
1429 1450
        ctx['email_is_unique'] = self.email_is_unique
1430 1451
        ctx['create'] = 'create' in self.request.GET
1431 1452
        return ctx
......
1544 1565
        return utils_misc.redirect(request, self.get_success_url())
1545 1566

  
1546 1567
    def send_registration_success_email(self, user):
1568
        # user may not have a registered email by then
1547 1569
        if not user.email:
1548 1570
            return
1549 1571

  
tests/settings.py
46 46
    'cache2.example.com',
47 47
]
48 48

  
49
KNOWN_SERVICES = {
50
    'wcs': {
51
        'default': {
52
            'title': 'test',
53
            'url': 'http://example.org',
54
            'secret': 'chrono',
55
            'orig': 'chrono',
56
            'backoffice-menu-url': 'http://example.org/manage/',
57
        }
58
    },
59
    'passerelle': {
60
        'default': {
61
            'title': 'test',
62
            'url': 'https://foo.whatever.none',
63
            'secret': 'passerelle',
64
            'orig': 'passerelle',
65
            'backoffice-menu-url': 'http://foo.whatever.none/manage/',
66
        }
67
    },
68
}
69

  
49 70
A2_AUTH_KERBEROS_ENABLED = False
50 71

  
51 72
A2_VALIDATE_EMAIL_DOMAIN = False
tests/test_registration.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from datetime import date
17
import json
18
from datetime import date, timedelta
18 19
from unittest import mock
19 20
from urllib.parse import urlparse
20 21

  
22
import mock
23
import requests
21 24
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
22 25
from django.urls import reverse
23 26
from django.utils.http import urlquote
27
from httmock import HTTMock, remember_called, urlmatch
24 28

  
25 29
from authentic2 import models
26 30
from authentic2.a2_rbac.utils import get_default_ou
27 31
from authentic2.apps.journal.models import Event
28 32
from authentic2.forms.profile import modelform_factory
29 33
from authentic2.forms.registration import RegistrationCompletionForm
34
from authentic2.models import SMSCode, Token
30 35
from authentic2.utils import misc as utils_misc
31 36
from authentic2.validators import EmailValidator
32 37

  
......
951 956
    resp = resp.form.submit()
952 957

  
953 958
    assert 'This password is not strong enough' in resp.text
959

  
960

  
961
@urlmatch(netloc='foo.whatever.none')
962
@remember_called
963
def sms_service_mock(url, request):
964
    return {
965
        'content': {},
966
        'headers': {
967
            'content-type': 'application/json',
968
        },
969
        'status_code': 200,
970
    }
971

  
972

  
973
def test_phone_registration_wrong_code(app, db, settings):
974
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
975
    settings.SMS_URL = 'https://foo.whatever.none/'
976

  
977
    resp = app.get(reverse('registration_register'))
978
    resp.form.set('phone_1', '612345678')
979
    with HTTMock(sms_service_mock):
980
        resp = resp.form.submit().follow()
981
    resp.form.set('registration_code', 'abc')
982
    resp = resp.form.submit()
983
    assert not Token.objects.count()
984
    assert resp.pyquery('li')[0].text_content() == 'Wrong registration code.'
985

  
986

  
987
def test_phone_registration_expired_code(app, db, settings, freezer):
988
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
989
    settings.SMS_URL = 'https://foo.whatever.none/'
990

  
991
    resp = app.get(reverse('registration_register'))
992
    resp.form.set('phone_1', '612345678')
993
    with HTTMock(sms_service_mock):
994
        resp = resp.form.submit().follow()
995
    code = SMSCode.objects.get()
996
    resp.form.set('registration_code', code.value)
997
    freezer.move_to(timedelta(hours=1))
998
    resp = resp.form.submit()
999
    assert not Token.objects.count()
1000
    assert resp.pyquery('li')[0].text_content() == 'The code has expired.'
1001

  
1002

  
1003
def test_phone_registration_cancel(app, db, settings, freezer):
1004
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
1005
    settings.SMS_URL = 'https://foo.whatever.none/'
1006

  
1007
    resp = app.get(reverse('registration_register'))
1008
    resp.form.set('phone_1', '612345678')
1009
    with HTTMock(sms_service_mock):
1010
        resp = resp.form.submit().follow()
1011
    code = SMSCode.objects.get()
1012
    resp.form.set('registration_code', code.value)
1013
    resp.form.submit('cancel').follow()
1014
    assert not Token.objects.count()
1015
    assert not SMSCode.objects.count()
1016

  
1017

  
1018
def test_phone_registration_improperly_configured(app, db, settings, freezer, caplog):
1019
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
1020
    settings.SMS_URL = ''
1021

  
1022
    resp = app.get(reverse('registration_register'))
1023
    resp.form.set('phone_1', '612345678')
1024
    with HTTMock(sms_service_mock):
1025
        resp = resp.form.submit().follow().maybe_follow()
1026
    assert not Token.objects.count()
1027
    assert not SMSCode.objects.count()
1028
    assert (
1029
        "Something went wrong while trying to send the SMS registration code to you"
1030
        in resp.pyquery('li.warning')[0].text_content()
1031
    )
1032
    assert caplog.records[0].message == 'settings.SMS_URL is not set'
1033

  
1034

  
1035
def test_phone_registration_connection_error(app, db, settings, freezer, caplog):
1036
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
1037
    settings.SMS_URL = 'https://foo.whatever.none/'
1038

  
1039
    def mocked_requests_connection_error(*args, **kwargs):
1040
        raise requests.ConnectionError('unreachable')
1041

  
1042
    resp = app.get(reverse('registration_register'))
1043
    resp.form.set('phone_1', '612345678')
1044
    with mock.patch('authentic2.utils.misc.Requests.send') as mock_send:
1045
        mock_send.side_effect = mocked_requests_connection_error
1046
        mock_response = mock.Mock(status_code=200)
1047
        mock_send.return_value = mock_response
1048
        resp = resp.form.submit().follow().maybe_follow()
1049
    assert (
1050
        "Something went wrong while trying to send the SMS registration code to you"
1051
        in resp.pyquery('li.warning')[0].text_content()
1052
    )
1053
    assert (
1054
        caplog.records[0].message
1055
        == 'sms registration to +33612345678 using https://foo.whatever.none/ failed: unreachable'
1056
    )
1057

  
1058

  
1059
def test_phone_registration(app, db, settings):
1060
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
1061
    settings.SMS_URL = 'https://foo.whatever.none/'
1062
    code_length = settings.SMS_CODE_LENGTH
1063

  
1064
    assert not SMSCode.objects.count()
1065
    assert not Token.objects.count()
1066
    resp = app.get(reverse('registration_register'))
1067
    resp.form.set('phone_1', '612345678')
1068
    with HTTMock(sms_service_mock):
1069
        resp = resp.form.submit().follow()
1070
        body = json.loads(sms_service_mock.call['requests'][0].body)
1071
    assert body['message'].startswith('Your code is')
1072
    code = SMSCode.objects.get()
1073
    assert body['message'][-code_length:] == code.value
1074
    resp.form.set('registration_code', code.value)
1075
    resp = resp.form.submit().follow()
1076
    assert Token.objects.count() == 1
1077

  
1078
    resp.form.set('password1', 'Password0')
1079
    resp.form.set('password2', 'Password0')
1080
    resp.form.set('first_name', 'John')
1081
    resp.form.set('last_name', 'Doe')
1082
    resp = resp.form.submit().follow()
1083
    assert "You have just created an account" in resp.text
1084

  
1085
    user = User.objects.get(first_name='John', last_name='Doe')
1086
    assert user.phone == '+33612345678'
954
-