handle phone-uniqueness settings at registration time (#82737)
gitea/authentic/pipeline/head This commit looks good Details

This commit is contained in:
Paul Marillonnet 2023-11-06 16:48:39 +01:00
parent fe87d258cc
commit b9da1b5764
7 changed files with 158 additions and 9 deletions

View File

@ -68,6 +68,7 @@ class OrganizationalUnitAdmin(admin.ModelAdmin):
'description',
'username_is_unique',
'email_is_unique',
'phone_is_unique',
'default',
'validate_emails',
'user_can_reset_password',

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2023-11-06 15:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('a2_rbac', '0037_remove_organizationalunit_min_password_strength'),
]
operations = [
migrations.AddField(
model_name='organizationalunit',
name='phone_is_unique',
field=models.BooleanField(blank=True, default=False, verbose_name='Phone is unique'),
),
]

View File

@ -104,6 +104,7 @@ class OrganizationalUnit(AbstractBase):
username_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Username is unique'))
email_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Email is unique'))
phone_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Phone is unique'))
default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit'))
validate_emails = models.BooleanField(blank=True, default=False, verbose_name=_('Validate emails'))
@ -234,6 +235,7 @@ class OrganizationalUnit(AbstractBase):
'description': self.description,
'default': self.default,
'email_is_unique': self.email_is_unique,
'phone_is_unique': self.phone_is_unique,
'username_is_unique': self.username_is_unique,
'validate_emails': self.validate_emails,
}

View File

@ -138,9 +138,13 @@ default_settings = dict(
A2_USER_CAN_RESET_PASSWORD_BY_USERNAME=Setting(
default=False, definition='Allow password reset request by username'
),
A2_EMAIL_IS_UNIQUE=Setting(default=False, definition='Email of users must be unique'),
A2_EMAIL_IS_UNIQUE=Setting(default=False, definition="Users' email address must be unique"),
A2_REGISTRATION_EMAIL_IS_UNIQUE=Setting(
default=False, definition='Email of registered accounts must be unique'
default=False, definition='Email address declared at registration time must be unique'
),
A2_PHONE_IS_UNIQUE=Setting(default=False, definition="Users' phone number must be unique"),
A2_REGISTRATION_PHONE_IS_UNIQUE=Setting(
default=False, definition='Phone number declared at registration time must be unique'
),
A2_REGISTRATION_FORM_USERNAME_REGEX=Setting(
default=r'^[\w.@+-]+$', definition='Regex to validate usernames'

View File

@ -1796,9 +1796,19 @@ class RegistrationCompletionView(CreateView):
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
if self.ou:
self.email_is_unique |= self.ou.email_is_unique
self.email_is_unique = self.phone_is_unique = False
if self.token.get('email', None):
self.email_is_unique = (
app_settings.A2_EMAIL_IS_UNIQUE or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
)
if self.ou:
self.email_is_unique |= self.ou.email_is_unique
elif self.token.get('phone', None) and self.authenticator.is_phone_authn_active:
self.phone_is_unique = (
app_settings.A2_PHONE_IS_UNIQUE or app_settings.A2_REGISTRATION_PHONE_IS_UNIQUE
)
if self.ou:
self.phone_is_unique |= self.ou.phone_is_unique
self.init_fields_labels_and_help_texts()
set_home_url(request, self.get_success_url())
return super().dispatch(request, *args, **kwargs)
@ -1921,12 +1931,18 @@ class RegistrationCompletionView(CreateView):
if hasattr(self, 'phone'):
ctx['phone'] = self.phone
ctx['email_is_unique'] = self.email_is_unique
ctx['phone_is_unique'] = self.phone_is_unique
ctx['create'] = 'create' in self.request.GET
return ctx
def get(self, request, *args, **kwargs):
if len(self.users) == 1 and self.email_is_unique:
# Found one user, EMAIL is unique, log her in
if len(self.users) == 1 and (
self.email_is_unique
and self.token.get('email', None)
or self.phone_is_unique
and self.token.get('phone', None)
):
# Found one user whose identifier is unique, log her in
utils_misc.simulate_authentication(request, self.users[0], method=self.authentication_method)
return utils_misc.redirect(request, self.get_success_url())
confirm_data = self.token.get('confirm_data', False)
@ -1956,8 +1972,13 @@ class RegistrationCompletionView(CreateView):
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if self.users and self.email_is_unique:
# email is unique, users already exist, creating a new one is forbidden !
if self.users and (
self.email_is_unique
and self.token.get('email', None)
or self.phone_is_unique
and self.token.get('phone', None)
):
# identifier is unique, users already exist, creating a new one is forbidden !
return utils_misc.redirect(
request, request.resolver_match.view_name, args=self.args, kwargs=self.kwargs
)

View File

@ -277,6 +277,7 @@ def test_ou_export_json(db):
description='basic ou description',
username_is_unique=True,
email_is_unique=True,
phone_is_unique=True,
default=False,
validate_emails=True,
)
@ -287,6 +288,7 @@ def test_ou_export_json(db):
assert ou_dict['description'] == ou.description
assert ou_dict['username_is_unique'] == ou.username_is_unique
assert ou_dict['email_is_unique'] == ou.email_is_unique
assert ou_dict['phone_is_unique'] == ou.phone_is_unique
assert ou_dict['default'] == ou.default
assert ou_dict['validate_emails'] == ou.validate_emails

View File

@ -1185,6 +1185,108 @@ def test_phone_registration_number_already_existing_select(app, db, settings, ph
assert app.session['_auth_user_id'] == str(user_ids[1])
def test_phone_registration_number_already_existing_phone_is_unique(app, db, settings, phone_activated_authn):
settings.SMS_URL = 'https://foo.whatever.none/'
settings.A2_PHONE_IS_UNIQUE = True
settings.A2_REGISTRATION_PHONE_IS_UNIQUE = True
# create duplicate
user = User.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
username='john',
ou=get_default_ou(),
)
user.attributes.phone = '+33612345678'
user.save()
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('sms_code', code.value)
resp = resp.form.submit().follow()
assert app.session['_auth_user_id'] == str(user.id)
# logged in user is redirected to their homepage
assert resp.location == '/'
def test_phone_registration_number_already_existing_registration_phone_is_unique(
app, db, settings, phone_activated_authn
):
settings.SMS_URL = 'https://foo.whatever.none/'
settings.A2_PHONE_IS_UNIQUE = False
settings.A2_REGISTRATION_PHONE_IS_UNIQUE = True
user_ids = []
# create duplicate
user = User.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
username='john',
ou=get_default_ou(),
)
user.attributes.phone = '+33612345678'
user.save()
user_ids.append(user.id)
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('sms_code', code.value)
resp = resp.form.submit().follow()
assert Token.objects.count() == 1
assert app.session['_auth_user_id'] == str(user.id)
# logged in user is redirected to their homepage
assert resp.location == '/'
def test_phone_registration_number_already_existing_ou_phone_is_unique(
app, db, settings, phone_activated_authn
):
settings.SMS_URL = 'https://foo.whatever.none/'
settings.A2_PHONE_IS_UNIQUE = False
settings.A2_REGISTRATION_PHONE_IS_UNIQUE = False
ou = get_default_ou()
ou.phone_is_unique = True
ou.save()
user_ids = []
# create duplicate
user = User.objects.create(
first_name='John',
last_name='Doe',
email='john@example.com',
username='john',
ou=ou,
)
user.attributes.phone = '+33612345678'
user.save()
user_ids.append(user.id)
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('sms_code', code.value)
resp = resp.form.submit().follow()
assert Token.objects.count() == 1
assert app.session['_auth_user_id'] == str(user.id)
# logged in user is redirected to their homepage
assert resp.location == '/'
def test_phone_registration(app, db, settings, phone_activated_authn):
settings.SMS_URL = 'https://foo.whatever.none/'
code_length = settings.SMS_CODE_LENGTH