Projet

Général

Profil

0001-custom_user-add-phone-and-phone-verification-fields-.patch

Paul Marillonnet, 19 septembre 2022 09:17

Télécharger (13 ko)

Voir les différences:

Subject: [PATCH] custom_user: add phone and phone verification fields (#65173)

 src/authentic2/app_settings.py                |  2 +-
 .../migrations/0032_auto_20220919_0848.py     | 55 +++++++++++++++++++
 src/authentic2/custom_user/models.py          | 24 +++++++-
 src/authentic2/validators.py                  |  6 ++
 tests/api/test_all.py                         | 31 ++++++-----
 tests/idp_oidc/test_misc.py                   |  4 +-
 tests/test_all.py                             |  3 +
 7 files changed, 106 insertions(+), 19 deletions(-)
 create mode 100644 src/authentic2/custom_user/migrations/0032_auto_20220919_0848.py
src/authentic2/app_settings.py
298 298
        default='3/d', definition='Maximum rate of emails sent to the same email address.'
299 299
    ),
300 300
    A2_USER_DELETED_KEEP_DATA=Setting(
301
        default=['email', 'uuid'], definition='User data to keep after deletion'
301
        default=['email', 'uuid', 'phone'], definition='User data to keep after deletion'
302 302
    ),
303 303
    A2_USER_DELETED_KEEP_DATA_DAYS=Setting(
304 304
        default=365, definition='Number of days to keep data on deleted users'
src/authentic2/custom_user/migrations/0032_auto_20220919_0848.py
1
# Generated by Django 2.2.26 on 2022-09-19 06:48
2

  
3
from django.db import migrations, models
4

  
5
import authentic2.validators
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('custom_user', '0031_profile_email'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='deleteduser',
17
            name='old_phone',
18
            field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Old phone number'),
19
        ),
20
        migrations.AddField(
21
            model_name='user',
22
            name='phone',
23
            field=models.CharField(
24
                blank=True,
25
                max_length=64,
26
                null=True,
27
                validators=[authentic2.validators.PhoneNumberValidator],
28
                verbose_name='phone number',
29
            ),
30
        ),
31
        migrations.AddField(
32
            model_name='user',
33
            name='phone_verified',
34
            field=models.BooleanField(default=False, verbose_name='phone verified'),
35
        ),
36
        migrations.AddField(
37
            model_name='user',
38
            name='phone_verified_date',
39
            field=models.DateTimeField(
40
                blank=True, default=None, null=True, verbose_name='phone verified date'
41
            ),
42
        ),
43
        migrations.AddConstraint(
44
            model_name='user',
45
            constraint=models.CheckConstraint(
46
                check=models.Q(
47
                    ('username__isnull', False),
48
                    ('email__isnull', False),
49
                    ('phone__isnull', False),
50
                    _connector='OR',
51
                ),
52
                name='constraint_at_least_one_identifier',
53
            ),
54
        ),
55
    ]
src/authentic2/custom_user/models.py
23 23
from django.core.exceptions import MultipleObjectsReturned, ValidationError
24 24
from django.core.mail import send_mail
25 25
from django.db import models, transaction
26
from django.db.models import Q
26 27
from django.urls import reverse
27 28
from django.utils import timezone
28 29
from django.utils.translation import gettext_lazy as _
......
42 43
from authentic2.utils import misc as utils_misc
43 44
from authentic2.utils.cache import RequestCache
44 45
from authentic2.utils.models import generate_slug
45
from authentic2.validators import email_validator
46
from authentic2.validators import PhoneNumberValidator, email_validator
46 47
from django_rbac.models import PermissionMixin
47 48

  
48 49
from .managers import UserManager, UserQuerySet
......
156 157
    email_verified_date = models.DateTimeField(
157 158
        default=None, blank=True, null=True, verbose_name=_('email verified date')
158 159
    )
160
    phone = models.CharField(
161
        _('phone number'), null=True, blank=True, max_length=64, validators=[PhoneNumberValidator]
162
    )
163
    phone_verified = models.BooleanField(default=False, verbose_name=_('phone verified'))
164
    phone_verified_date = models.DateTimeField(
165
        default=None, blank=True, null=True, verbose_name=_('phone verified date')
166
    )
159 167
    is_staff = models.BooleanField(
160 168
        _('staff status'),
161 169
        default=False,
......
202 210
        verbose_name = _('user')
203 211
        verbose_name_plural = _('users')
204 212
        ordering = ('last_name', 'first_name', 'email', 'username')
213
        constraints = [
214
            models.CheckConstraint(
215
                check=Q(username__isnull=False) | Q(email__isnull=False) | Q(phone__isnull=False),
216
                name='constraint_at_least_one_identifier',
217
            )
218
        ]
205 219

  
206 220
    def get_full_name(self):
207 221
        """
......
406 420
            deleted_user.old_email = self.email.rsplit('#', 1)[0]
407 421
        if 'uuid' in app_settings.A2_USER_DELETED_KEEP_DATA:
408 422
            deleted_user.old_uuid = self.uuid
423
        if 'phone' in app_settings.A2_USER_DELETED_KEEP_DATA:
424
            deleted_user.old_phone = self.phone
409 425

  
410 426
        # save LDAP account references
411 427
        external_ids = self.userexternalid_set.order_by('id')
......
454 470
    old_uuid = models.TextField(verbose_name=_('Old UUID'), null=True, blank=True)
455 471
    old_user_id = models.PositiveIntegerField(verbose_name=_('Old user id'), null=True, blank=True)
456 472
    old_email = models.EmailField(verbose_name=_('Old email adress'), null=True, blank=True)
473
    old_phone = models.CharField(verbose_name=_('Old phone number'), null=True, blank=True, max_length=64)
457 474
    old_data = JSONField(verbose_name=_('Old data'), null=True, blank=True)
458 475

  
459 476
    @classmethod
......
464 481
        cls.objects.filter(deleted__lt=threshold).delete()
465 482

  
466 483
    def __repr__(self):
467
        return 'DeletedUser(old_id=%s, old_uuid=%s…, old_email=%s)' % (
484
        return 'DeletedUser(old_id=%s, old_uuid=%s…, old_email=%s, old_phone=%s)' % (
468 485
            self.old_user_id or '-',
469 486
            (self.old_uuid or '')[:6],
470 487
            self.old_email or '-',
488
            self.old_phone or '-',
471 489
        )
472 490

  
473 491
    def __str__(self):
474 492
        data = ['#%d' % self.old_user_id]
475 493
        if self.old_email:
476 494
            data.append(self.old_email)
495
        if self.old_phone:
496
            data.append(self.old_phone)
477 497
        return _('deleted user (%s)') % ', '.join(data)
478 498

  
479 499
    class Meta:
src/authentic2/validators.py
99 99
email_validator = EmailValidator()
100 100

  
101 101

  
102
class PhoneNumberValidator(RegexValidator):
103
    def __init__(self, *args, **kwargs):
104
        self.regex = r'^\+?\d{,20}$'
105
        super().__init__(*args, **kwargs)
106

  
107

  
102 108
class UsernameValidator(RegexValidator):
103 109
    def __init__(self, *args, **kwargs):
104 110
        self.regex = app_settings.A2_REGISTRATION_FORM_USERNAME_REGEX
tests/api/test_all.py
66 66
    'modified',
67 67
    'email_verified',
68 68
    'email_verified_date',
69
    'phone',
70
    'phone_verified',
71
    'phone_verified_date',
69 72
    'last_account_deletion_alert',
70 73
    'deactivation',
71 74
    'deactivation_reason',
......
2390 2393

  
2391 2394
def test_phone_normalization_ok(settings, app, admin):
2392 2395
    headers = basic_authorization_header(admin)
2393
    Attribute.objects.create(name='phone', label='phone', kind='phone_number')
2396
    Attribute.objects.create(name='extra_phone', label='extra phone', kind='phone_number')
2394 2397
    payload = {
2395 2398
        'username': 'janedoe',
2396
        'phone': ' + 334-99 98.56/43',
2399
        'extra_phone': ' + 334-99 98.56/43',
2397 2400
        'first_name': 'Jane',
2398 2401
        'last_name': 'Doe',
2399 2402
    }
2400 2403
    resp = app.post_json('/api/users/', headers=headers, params=payload, status=201)
2401
    assert resp.json['phone'] == '+33499985643'
2402
    assert User.objects.get(username='janedoe').attributes.phone == '+33499985643'
2404
    assert resp.json['extra_phone'] == '+33499985643'
2405
    assert User.objects.get(username='janedoe').attributes.extra_phone == '+33499985643'
2403 2406

  
2404 2407

  
2405 2408
def test_phone_normalization_nok(settings, app, admin):
2406 2409
    headers = basic_authorization_header(admin)
2407
    Attribute.objects.create(name='phone', label='phone', kind='phone_number')
2410
    Attribute.objects.create(name='extra_phone', label='extra phone', kind='phone_number')
2408 2411
    payload = {
2409 2412
        'username': 'janedoe',
2410 2413
        'first_name': 'Jane',
2411 2414
        'last_name': 'Doe',
2412 2415
    }
2413
    payload['phone'] = (' + 334-99+98.56/43',)
2416
    payload['extra_phone'] = (' + 334-99+98.56/43',)
2414 2417
    app.post_json('/api/users/', headers=headers, params=payload, status=400)
2415 2418

  
2416
    payload['phone'] = '1#2'
2419
    payload['extra_phone'] = '1#2'
2417 2420
    app.post_json('/api/users/', headers=headers, params=payload, status=400)
2418 2421

  
2419 2422

  
2420 2423
def test_fr_phone_normalization_ok(settings, app, admin):
2421 2424
    headers = basic_authorization_header(admin)
2422
    Attribute.objects.create(name='phone', label='phone', kind='fr_phone_number')
2425
    Attribute.objects.create(name='extra_phone', label='extra phone', kind='fr_phone_number')
2423 2426
    payload = {
2424 2427
        'username': 'janedoe',
2425
        'phone': ' 04-99 98.56/43',
2428
        'extra_phone': ' 04-99 98.56/43',
2426 2429
        'first_name': 'Jane',
2427 2430
        'last_name': 'Doe',
2428 2431
    }
2429 2432
    resp = app.post_json('/api/users/', headers=headers, params=payload, status=201)
2430
    assert resp.json['phone'] == '0499985643'
2431
    assert User.objects.get(username='janedoe').attributes.phone == '0499985643'
2433
    assert resp.json['extra_phone'] == '0499985643'
2434
    assert User.objects.get(username='janedoe').attributes.extra_phone == '0499985643'
2432 2435

  
2433 2436

  
2434 2437
def test_fr_phone_normalization_nok(settings, app, admin):
2435 2438
    headers = basic_authorization_header(admin)
2436
    Attribute.objects.create(name='phone', label='phone', kind='fr_phone_number')
2439
    Attribute.objects.create(name='extra_phone', label='extra phone', kind='fr_phone_number')
2437 2440
    payload = {
2438 2441
        'username': 'janedoe',
2439
        'phone': '+33499985643',
2442
        'extra_phone': '+33499985643',
2440 2443
        'first_name': 'Jane',
2441 2444
        'last_name': 'Doe',
2442 2445
    }
2443 2446
    app.post_json('/api/users/', headers=headers, params=payload, status=400)
2444 2447

  
2445
    payload['phone'] = '1#2'
2448
    payload['extra_phone'] = '1#2'
2446 2449
    app.post_json('/api/users/', headers=headers, params=payload, status=400)
2447 2450

  
2448 2451

  
tests/idp_oidc/test_misc.py
1167 1167
    assert claims['given_name'] == simple_user.first_name
1168 1168
    assert claims['family_name'] == simple_user.last_name
1169 1169
    assert claims['email'] == simple_user.email
1170
    assert claims['phone'] is None
1170
    assert claims['phone'] == simple_user.phone
1171 1171
    assert claims['email_verified'] is False
1172 1172

  
1173 1173
    assert user_info['sub'] == make_sub(oidc_client, simple_user)
......
1175 1175
    assert user_info['given_name'] == simple_user.first_name
1176 1176
    assert user_info['family_name'] == simple_user.last_name
1177 1177
    assert user_info['email'] == simple_user.email
1178
    assert user_info['phone'] is None
1178
    assert user_info['phone'] == simple_user.phone
1179 1179
    assert user_info['email_verified'] is False
1180 1180

  
1181 1181
    params['scope'] = 'openid email'
tests/test_all.py
73 73
                    'email_verified_date': None,
74 74
                    'username': 'john.doe',
75 75
                    'email': '',
76
                    'phone': None,
77
                    'phone_verified': False,
78
                    'phone_verified_date': None,
76 79
                    'first_name': '',
77 80
                    'last_name': '',
78 81
                    'is_active': True,
79
-