Projet

Général

Profil

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

Paul Marillonnet, 19 septembre 2022 15:21

Télécharger (15,7 ko)

Voir les différences:

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

 src/authentic2/admin.py                       | 21 ++++++--
 src/authentic2/app_settings.py                |  2 +-
 .../migrations/0032_auto_20220919_1230.py     | 50 +++++++++++++++++++
 src/authentic2/custom_user/models.py          | 32 ++++++++++--
 src/authentic2/validators.py                  |  6 +++
 tests/api/test_all.py                         | 30 +++++------
 tests/idp_oidc/test_misc.py                   |  4 +-
 tests/test_all.py                             |  2 +
 8 files changed, 122 insertions(+), 25 deletions(-)
 create mode 100644 src/authentic2/custom_user/migrations/0032_auto_20220919_1230.py
src/authentic2/admin.py
286 286
class AuthenticUserAdmin(UserAdmin):
287 287
    fieldsets = (
288 288
        (None, {'fields': ('uuid', 'ou', 'password')}),
289
        (_('Personal info'), {'fields': ('username', 'first_name', 'last_name', 'email', 'email_verified')}),
289
        (
290
            _('Personal info'),
291
            {
292
                'fields': (
293
                    'username',
294
                    'first_name',
295
                    'last_name',
296
                    'email',
297
                    'email_verified',
298
                    'phone',
299
                    'phone_verified_on',
300
                )
301
            },
302
        ),
290 303
        (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups')}),
291 304
        (_('Important dates'), {'fields': ('last_login', 'date_joined', 'deactivation')}),
292 305
    )
......
302 315
                    'last_name',
303 316
                    'email',
304 317
                    'email_verified',
318
                    'phone',
319
                    'phone_verified_on',
305 320
                    'password1',
306 321
                    'password2',
307 322
                ),
......
310 325
    )
311 326
    readonly_fields = ('uuid',)
312 327
    list_filter = UserAdmin.list_filter + (UserRealmListFilter, ExternalUserListFilter)
313
    list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email']
328
    list_display = ['__str__', 'ou', 'first_name', 'last_name', 'email', 'phone']
314 329
    actions = UserAdmin.actions + ['mark_as_inactive']
315 330

  
316 331
    def get_fieldsets(self, request, obj=None):
......
351 366
            qs = models.Attribute.objects.all()
352 367
        else:
353 368
            qs = models.Attribute.objects.filter(required=True)
354
        non_model_fields = {a.name for a in qs} - {'first_name', 'last_name'}
369
        non_model_fields = {a.name for a in qs} - {'first_name', 'last_name', 'phone'}
355 370
        fields = list(set(fields) - set(non_model_fields))
356 371
        kwargs['fields'] = fields
357 372
        return super().get_form(request, obj=obj, **kwargs)
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_1230.py
1
# Generated by Django 2.2.26 on 2022-09-19 10:30
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_on',
34
            field=models.DateTimeField(
35
                blank=True, default=None, null=True, verbose_name='phone verification date'
36
            ),
37
        ),
38
        migrations.AddConstraint(
39
            model_name='user',
40
            constraint=models.CheckConstraint(
41
                check=models.Q(
42
                    ('username__isnull', False),
43
                    ('email__isnull', False),
44
                    ('phone__isnull', False),
45
                    _connector='OR',
46
                ),
47
                name='constraint_at_least_one_identifier',
48
            ),
49
        ),
50
    ]
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_on = models.DateTimeField(
164
        null=True,
165
        blank=True,
166
        default=None,
167
        verbose_name=_('phone verification date'),
168
    )
159 169
    is_staff = models.BooleanField(
160 170
        _('staff status'),
161 171
        default=False,
......
202 212
        verbose_name = _('user')
203 213
        verbose_name_plural = _('users')
204 214
        ordering = ('last_name', 'first_name', 'email', 'username')
215
        constraints = [
216
            models.CheckConstraint(
217
                check=Q(username__isnull=False) | Q(email__isnull=False) | Q(phone__isnull=False),
218
                name='constraint_at_least_one_identifier',
219
            )
220
        ]
205 221

  
206 222
    def get_full_name(self):
207 223
        """
......
247 263
        return '<User: %s (%s)>' % (human_name, short_id)
248 264

  
249 265
    def clean(self):
250
        if not (self.username or self.email or (self.first_name and self.last_name)):
266
        if not (self.username or self.email or self.phone or (self.first_name and self.last_name)):
251 267
            raise ValidationError(
252 268
                _(
253
                    'An account needs at least one identifier: username, email or a full name (first and last'
254
                    ' name).'
269
                    'An account needs at least one identifier: username, email, phone numbor or a full name '
270
                    '(first and last name).'
255 271
                )
256 272
            )
257 273

  
......
406 422
            deleted_user.old_email = self.email.rsplit('#', 1)[0]
407 423
        if 'uuid' in app_settings.A2_USER_DELETED_KEEP_DATA:
408 424
            deleted_user.old_uuid = self.uuid
425
        if 'phone' in app_settings.A2_USER_DELETED_KEEP_DATA:
426
            deleted_user.old_phone = self.phone
409 427

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

  
459 478
    @classmethod
......
464 483
        cls.objects.filter(deleted__lt=threshold).delete()
465 484

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

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

  
479 501
    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_on',
69 71
    'last_account_deletion_alert',
70 72
    'deactivation',
71 73
    'deactivation_reason',
......
2390 2392

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

  
2404 2406

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

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

  
2419 2421

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

  
2433 2435

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

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

  
2448 2450

  
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_on': None,
76 78
                    'first_name': '',
77 79
                    'last_name': '',
78 80
                    'is_active': True,
79
-