Permettre une création de compte par numéro de téléphone via l’API d’enregistrement (#83190) #167
|
@ -145,6 +145,7 @@ def extract_settings_from_environ():
|
|||
'A2_CAN_RESET_PASSWORD',
|
||||
'A2_REGISTRATION_CAN_DELETE_ACCOUNT',
|
||||
'A2_REGISTRATION_EMAIL_IS_UNIQUE',
|
||||
'A2_REGISTRATION_PHONE_IS_UNIQUE',
|
||||
'REGISTRATION_OPEN',
|
||||
'A2_AUTH_PASSWORD_ENABLE',
|
||||
'SSLAUTH_ENABLE',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.db import models, transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -64,10 +65,11 @@ from .a2_rbac.utils import get_default_ou
|
|||
from .apps.journal.models import Event
|
||||
from .custom_user.models import Profile, ProfileType, User
|
||||
from .journal_event_types import UserLogin, UserRegistration
|
||||
from .models import APIClient, Attribute, PasswordReset, Service
|
||||
from .models import APIClient, Attribute, AttributeValue, PasswordReset, Service
|
||||
from .passwords import get_password_checker, get_password_strength
|
||||
from .utils import hooks
|
||||
from .utils import misc as utils_misc
|
||||
from .utils import sms as utils_sms
|
||||
from .utils.api import DjangoRBACPermission, NaturalKeyRelatedField
|
||||
from .utils.lookups import Unaccent
|
||||
|
||||
|
@ -132,10 +134,12 @@ class RegistrationSerializer(serializers.Serializer):
|
|||
allow_null=True,
|
||||
)
|
||||
username = serializers.CharField(required=False, allow_blank=True)
|
||||
phone = serializers.CharField(required=False, allow_blank=True)
|
||||
first_name = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
last_name = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
password = serializers.CharField(required=False, allow_null=True)
|
||||
no_email_validation = serializers.BooleanField(required=False)
|
||||
no_phone_validation = serializers.BooleanField(required=False)
|
||||
return_url = serializers.URLField(required=False, allow_blank=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
|
@ -155,25 +159,60 @@ class RegistrationSerializer(serializers.Serializer):
|
|||
if 'email' not in attrs:
|
||||
raise serializers.ValidationError(_('Email is required'))
|
||||
if User.objects.filter(email__iexact=attrs['email']).exists():
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
raise serializers.ValidationError(_('Account already exists when looking up by email'))
|
||||
|
||||
if ou.email_is_unique:
|
||||
if 'email' not in attrs:
|
||||
raise serializers.ValidationError(_('Email is required in this ou'))
|
||||
if User.objects.filter(ou=ou, email__iexact=attrs['email']).exists():
|
||||
raise serializers.ValidationError(_('Account already exists in this ou'))
|
||||
raise serializers.ValidationError(
|
||||
_('Account already exists in this ou when looking up by email')
|
||||
)
|
||||
|
||||
if app_settings.A2_USERNAME_IS_UNIQUE or app_settings.A2_REGISTRATION_USERNAME_IS_UNIQUE:
|
||||
if 'username' not in attrs:
|
||||
raise serializers.ValidationError(_('Username is required'))
|
||||
if User.objects.filter(username=attrs['username']).exists():
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
raise serializers.ValidationError(_('Account already exists when looking up by username'))
|
||||
|
||||
if ou.username_is_unique:
|
||||
if 'username' not in attrs:
|
||||
raise serializers.ValidationError(_('Username is required in this ou'))
|
||||
if User.objects.filter(ou=ou, username=attrs['username']).exists():
|
||||
raise serializers.ValidationError(_('Account already exists in this ou'))
|
||||
raise serializers.ValidationError(
|
||||
_('Account already exists in this ou when looking up by username')
|
||||
)
|
||||
|
||||
user_ct = ContentType.objects.get_for_model(User)
|
||||
phone_field = utils_misc.get_password_authenticator().phone_identifier_field
|
||||
|
||||
if phone_field and (
|
||||
app_settings.A2_PHONE_IS_UNIQUE or app_settings.A2_REGISTRATION_PHONE_IS_UNIQUE
|
||||
):
|
||||
if 'phone' not in attrs:
|
||||
raise serializers.ValidationError(_('Phone is required'))
|
||||
if AttributeValue.objects.filter(
|
||||
content=attrs['phone'],
|
||||
attribute=phone_field,
|
||||
content_type=user_ct,
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
_('Account already exists when looking up by phone number')
|
||||
)
|
||||
|
||||
if ou.phone_is_unique:
|
||||
if 'phone' not in attrs:
|
||||
raise serializers.ValidationError(_('Phone is required'))
|
||||
known_owner_ids = AttributeValue.objects.filter(
|
||||
content=attrs['phone'],
|
||||
attribute=phone_field,
|
||||
content_type=user_ct,
|
||||
).values_list('object_id', flat=True)
|
||||
if ou.id in set(User.objects.filter(id__in=known_owner_ids).values_list('ou', flat=True)):
|
||||
raise serializers.ValidationError(
|
||||
_('Account already exists in this ou when looking up by phone number')
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
|
@ -216,7 +255,6 @@ class Register(BaseRpcView):
|
|||
raise PermissionDenied(
|
||||
'You do not have permission to create users in ou %s' % validated_data['ou'].slug
|
||||
)
|
||||
email = validated_data.get('email')
|
||||
registration_data = {}
|
||||
for field in ('first_name', 'last_name', 'password', 'username'):
|
||||
if field in validated_data:
|
||||
|
@ -228,6 +266,8 @@ class Register(BaseRpcView):
|
|||
'registration_data': registration_data,
|
||||
}
|
||||
|
||||
email = validated_data.get('email')
|
||||
phone = validated_data.get('phone')
|
||||
token = None
|
||||
final_return_url = None
|
||||
if validated_data.get('return_url'):
|
||||
|
@ -262,18 +302,40 @@ class Register(BaseRpcView):
|
|||
if token:
|
||||
response['token'] = token
|
||||
response_status = status.HTTP_202_ACCEPTED
|
||||
elif phone and not validated_data.get('no_phone_validation'):
|
||||
try:
|
||||
# XXX custom code lifetime for registration API?
|
||||
code = utils_sms.send_registration_sms(
|
||||
phone,
|
||||
validated_data['ou'],
|
||||
)
|
||||
except utils_sms.SMSError as e:
|
||||
response = {
|
||||
'result': 0,
|
||||
'errors': {'__all__': ['SMS sending failed']},
|
||||
'exception': force_str(e),
|
||||
}
|
||||
response_status = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
else:
|
||||
response = {
|
||||
'result': 1,
|
||||
}
|
||||
if token:
|
||||
response['token'] = token
|
||||
response['validation_url'] = reverse('input_sms_code', kwargs={'token': code.url_token})
|
||||
response_status = status.HTTP_202_ACCEPTED
|
||||
else:
|
||||
username = validated_data.get('username')
|
||||
first_name = validated_data.get('first_name')
|
||||
last_name = validated_data.get('last_name')
|
||||
password = validated_data.get('password')
|
||||
ou = validated_data.get('ou')
|
||||
if not email and not username and not (first_name and last_name):
|
||||
if not email and not phone and not username and not (first_name and last_name):
|
||||
response = {
|
||||
'result': 0,
|
||||
'errors': {
|
||||
'__all__': [
|
||||
'You must set at least a username, an email or a first name and a last name'
|
||||
'You must set at least a username, an email, a phone number, or first and last names'
|
||||
]
|
||||
},
|
||||
}
|
||||
|
@ -285,6 +347,13 @@ class Register(BaseRpcView):
|
|||
if password:
|
||||
new_user.set_password(password)
|
||||
new_user.save()
|
||||
if phone:
|
||||
AttributeValue.objects.create(
|
||||
attribute=utils_misc.get_password_authenticator().phone_identifier_field,
|
||||
content=phone,
|
||||
object_id=new_user.id,
|
||||
content_type=ContentType.objects.get_for_model(new_user),
|
||||
)
|
||||
validated_data['uuid'] = new_user.uuid
|
||||
response = {
|
||||
'result': 1,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -7,8 +7,17 @@
|
|||
|
||||
{% block content %}
|
||||
<h2>{% trans "Login" %}</h2>
|
||||
{% if email %}
|
||||
<p>
|
||||
{% blocktrans trimmed count accounts_number=accounts|length %}An account already exists for this email address.{% plural %}Existing accounts are associated with this email address.{% endblocktrans %}
|
||||
</p>
|
||||
{% elif phone %}
|
||||
<p>
|
||||
{% blocktrans trimmed count accounts_number=accounts|length %}An account already exists for this phone number.{% plural %}Existing accounts are associated with this phone number.{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{% blocktrans trimmed count accounts_number=accounts|length %}An account already exists for this email. Please click on the account name to log in with.{% plural %}More accounts are associated to this email. Please choose the account you want to log in with:{% endblocktrans %}
|
||||
{% blocktrans trimmed count accounts_number=accounts|length %} Please click on the account name to log in with.{% plural %}Please choose the account you want to log in with.{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
|
|
|
@ -1776,9 +1776,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)
|
||||
|
@ -1901,12 +1911,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)
|
||||
|
@ -1936,8 +1952,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
|
||||
)
|
||||
|
|
|
@ -32,6 +32,7 @@ from django.urls import reverse
|
|||
from django.utils.encoding import force_str
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from httmock import HTTMock, remember_called, urlmatch
|
||||
from requests.models import Response
|
||||
|
||||
from authentic2.a2_rbac.models import SEARCH_OP
|
||||
|
@ -41,7 +42,15 @@ from authentic2.a2_rbac.utils import get_default_ou
|
|||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.apps.journal.models import Event, EventType
|
||||
from authentic2.custom_user.models import Profile, ProfileType
|
||||
from authentic2.models import APIClient, Attribute, AttributeValue, AuthorizedRole, PasswordReset, Service
|
||||
from authentic2.models import (
|
||||
APIClient,
|
||||
Attribute,
|
||||
AttributeValue,
|
||||
AuthorizedRole,
|
||||
PasswordReset,
|
||||
Service,
|
||||
SMSCode,
|
||||
)
|
||||
from authentic2.utils.misc import good_next_url
|
||||
from authentic2_idp_cas.models import Service as CASService
|
||||
|
||||
|
@ -85,6 +94,18 @@ USER_ATTRIBUTES_SET = {
|
|||
}
|
||||
|
||||
|
||||
@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_api_user_simple(logged_app):
|
||||
resp = logged_app.get('/api/user/')
|
||||
assert isinstance(resp.json, dict)
|
||||
|
@ -1101,7 +1122,7 @@ def test_register_no_email_validation(settings, app, admin, django_user_model):
|
|||
assert 'errors' in response.json
|
||||
assert response.json['result'] == 0
|
||||
assert response.json['errors'] == {
|
||||
'__all__': ['You must set at least a username, an email or a first name and a last name'],
|
||||
'__all__': ['You must set at least a username, an email, a phone number, or first and last names'],
|
||||
}
|
||||
|
||||
# valid payload
|
||||
|
@ -1159,7 +1180,7 @@ def test_register_ou_no_email_validation(settings, app, admin, django_user_model
|
|||
assert 'errors' in response.json
|
||||
assert response.json['result'] == 0
|
||||
assert response.json['errors'] == {
|
||||
'__all__': ['You must set at least a username, an email or a first name and a last name'],
|
||||
'__all__': ['You must set at least a username, an email, a phone number, or first and last names'],
|
||||
}
|
||||
|
||||
# valid payload
|
||||
|
@ -2658,7 +2679,7 @@ def test_api_users_create_user_delete(app, settings, admin):
|
|||
assert resp.json['errors'] == {'email': ['email already used']}
|
||||
|
||||
|
||||
def test_api_register_user_delete(app, settings, admin):
|
||||
def test_api_register_user_delete_with_email_identifier(app, settings, admin):
|
||||
settings.A2_EMAIL_IS_UNIQUE = True
|
||||
|
||||
user = User.objects.create(username='foo', email='john.doe@example.com', ou=get_default_ou())
|
||||
|
@ -2675,12 +2696,91 @@ def test_api_register_user_delete(app, settings, admin):
|
|||
'ou': 'default',
|
||||
}
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=400)
|
||||
assert response.json['errors'] == {'__all__': ['Account already exists']}
|
||||
assert response.json['errors'] == {'__all__': ['Account already exists when looking up by email']}
|
||||
|
||||
user.delete()
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=201)
|
||||
|
||||
|
||||
def test_api_register_user_delete_with_phone_no_identifier_validation(
|
||||
app, settings, admin, phone_activated_authn
|
||||
):
|
||||
settings.A2_PHONE_IS_UNIQUE = True
|
||||
|
||||
user = User.objects.create(username='foo', ou=get_default_ou())
|
||||
user.attributes.phone = '+33111221133'
|
||||
user.save()
|
||||
|
||||
headers = basic_authorization_header(admin)
|
||||
payload = {
|
||||
'username': 'john.doe',
|
||||
'phone': '+33111221133',
|
||||
'email': 'john.doe@example.com',
|
||||
'first_name': 'John',
|
||||
'last_name': 'Doe',
|
||||
'password': '12XYab',
|
||||
'no_phone_validation': True,
|
||||
'no_email_validation': True,
|
||||
'return_url': 'http://sp.example.com/validate/',
|
||||
'ou': 'default',
|
||||
}
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=400)
|
||||
assert response.json['errors'] == {'__all__': ['Account already exists when looking up by phone number']}
|
||||
|
||||
user.delete()
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=201)
|
||||
assert User.objects.get(email='john.doe@example.com').attributes.phone == '+33111221133'
|
||||
|
||||
|
||||
def test_api_register_user_delete_with_phone_validation(app, settings, admin, phone_activated_authn):
|
||||
settings.A2_PHONE_IS_UNIQUE = True
|
||||
|
||||
user = User.objects.create(username='foo', ou=get_default_ou())
|
||||
user.attributes.phone = '+33111221133'
|
||||
user.save()
|
||||
|
||||
headers = basic_authorization_header(admin)
|
||||
payload = {
|
||||
'username': 'john.doe',
|
||||
'phone': '+33111221133',
|
||||
'first_name': 'John',
|
||||
'last_name': 'Doe',
|
||||
'password': '12XYab',
|
||||
'return_url': 'http://sp.example.com/validate/',
|
||||
'ou': 'default',
|
||||
}
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=400)
|
||||
assert response.json['errors'] == {'__all__': ['Account already exists when looking up by phone number']}
|
||||
|
||||
user.delete()
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=503)
|
||||
assert response.json['errors'] == {'__all__': ['SMS sending failed']}
|
||||
|
||||
assert SMSCode.objects.count() == 0
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
with HTTMock(sms_service_mock):
|
||||
response = app.post_json(reverse('a2-api-register'), params=payload, headers=headers, status=202)
|
||||
assert SMSCode.objects.count() == 1
|
||||
|
||||
response = app.get(response.json['validation_url'])
|
||||
form = response.form
|
||||
form.set('sms_code', SMSCode.objects.get().value)
|
||||
response = form.submit().follow()
|
||||
|
||||
# XXX even when validation happens, fields should be pre-filled
|
||||
# store registration_data in SMSCode then in Token (?)
|
||||
form = response.form
|
||||
form.set('first_name', 'Didier')
|
||||
form.set('last_name', 'Toto')
|
||||
form.set('password1', 'Bépo7390$$!')
|
||||
form.set('password2', 'Bépo7390$$!')
|
||||
response = form.submit()
|
||||
assert response.location == '/'
|
||||
|
||||
response = response.follow()
|
||||
assert response.pyquery('ul.messages li').text() == 'You have just created an account.'
|
||||
|
||||
|
||||
def test_api_password_change_user_delete(app, settings, admin, ou1):
|
||||
app.authorization = ('Basic', (admin.username, admin.username))
|
||||
user1 = User.objects.create(username='john.doe', email='john.doe@example.com', ou=ou1)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ from rest_framework import status, test
|
|||
|
||||
from authentic2 import attribute_kinds, models
|
||||
from authentic2.a2_rbac.models import OrganizationalUnit, Role
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.utils import misc as utils_misc
|
||||
from authentic2.utils.misc import continue_to_next_url, login_require, make_url, redirect, redirect_to_login
|
||||
|
||||
|
@ -394,7 +395,7 @@ class APITest(TestCase):
|
|||
ct_user = ContentType.objects.get_for_model(User)
|
||||
|
||||
self.ou = OrganizationalUnit.objects.create(
|
||||
slug='ou', name='OU', email_is_unique=True, username_is_unique=True
|
||||
slug='ou', name='OU', email_is_unique=True, username_is_unique=True, phone_is_unique=False
|
||||
)
|
||||
self.reguser1 = User.objects.create(username='reguser1')
|
||||
self.reguser1.set_password('password')
|
||||
|
@ -493,7 +494,10 @@ class APITest(TestCase):
|
|||
response = client.post(
|
||||
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
|
||||
)
|
||||
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists in this ou')])
|
||||
self.assertEqual(
|
||||
response.data['errors']['__all__'],
|
||||
[_('Account already exists in this ou when looking up by email')],
|
||||
)
|
||||
# Username is required
|
||||
payload = {
|
||||
'email': '1' + email,
|
||||
|
@ -516,7 +520,9 @@ class APITest(TestCase):
|
|||
response = client.post(
|
||||
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
|
||||
)
|
||||
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists')])
|
||||
self.assertEqual(
|
||||
response.data['errors']['__all__'], [_('Account already exists when looking up by username')]
|
||||
)
|
||||
|
||||
def test_register_reguser2_wrong_ou(self):
|
||||
client = test.APIClient()
|
||||
|
@ -539,10 +545,21 @@ class APITest(TestCase):
|
|||
self.assertIn('errors', response.data)
|
||||
|
||||
@override_settings(A2_REQUIRED_FIELDS=['username'])
|
||||
def test_email_is_unique_double_registration(self):
|
||||
def test_identifier_is_unique_double_registration(self):
|
||||
# disable existing attributes
|
||||
models.Attribute.objects.update(disabled=True)
|
||||
|
||||
phone, dummy = models.Attribute.objects.get_or_create(
|
||||
name='phone',
|
||||
kind='phone_number',
|
||||
user_editable=True,
|
||||
defaults={'label': 'Phone'},
|
||||
)
|
||||
LoginPasswordAuthenticator.objects.update(
|
||||
accept_phone_authentication=True,
|
||||
phone_identifier_field=phone,
|
||||
)
|
||||
|
||||
cred = self.reguser3_cred
|
||||
User = get_user_model()
|
||||
user_count = User.objects.count()
|
||||
|
@ -550,10 +567,12 @@ class APITest(TestCase):
|
|||
password = '12=XY=ab'
|
||||
username = 'john.doe'
|
||||
email = 'john.doe@example.com'
|
||||
phone = '+3311221133'
|
||||
return_url = 'http://sp.org/register/'
|
||||
payload = {
|
||||
'email': email,
|
||||
'username': username,
|
||||
'phone': phone,
|
||||
'ou': self.ou.slug,
|
||||
'password': password,
|
||||
'return_url': return_url,
|
||||
|
@ -599,7 +618,10 @@ class APITest(TestCase):
|
|||
response = client.post(
|
||||
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
|
||||
)
|
||||
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists in this ou')])
|
||||
self.assertEqual(
|
||||
response.data['errors']['__all__'],
|
||||
[_('Account already exists in this ou when looking up by email')],
|
||||
)
|
||||
# Username is required
|
||||
payload = {
|
||||
'email': '1' + email,
|
||||
|
@ -622,7 +644,42 @@ class APITest(TestCase):
|
|||
response = client.post(
|
||||
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
|
||||
)
|
||||
self.assertEqual(response.data['errors']['__all__'], [_('Account already exists')])
|
||||
self.assertEqual(
|
||||
response.data['errors']['__all__'], [_('Account already exists when looking up by username')]
|
||||
)
|
||||
|
||||
last_user.attributes.phone = phone
|
||||
last_user.save()
|
||||
self.ou.phone_is_unique = True
|
||||
self.ou.save()
|
||||
# Phone is required
|
||||
payload = {
|
||||
'email': '1' + email,
|
||||
'username': '1' + username,
|
||||
'ou': self.ou.slug,
|
||||
'password': password,
|
||||
'return_url': return_url,
|
||||
}
|
||||
response = client.post(
|
||||
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
|
||||
)
|
||||
self.assertEqual(response.data['errors']['__all__'], [_('Phone is required')])
|
||||
# Test phone is unique
|
||||
payload = {
|
||||
'email': '1' + email,
|
||||
'username': '1' + username,
|
||||
'phone': phone,
|
||||
'ou': self.ou.slug,
|
||||
'password': password,
|
||||
'return_url': return_url,
|
||||
}
|
||||
response = client.post(
|
||||
reverse('a2-api-register'), content_type='application/json', data=json.dumps(payload)
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['errors']['__all__'],
|
||||
[_('Account already exists in this ou when looking up by phone number')],
|
||||
)
|
||||
|
||||
@override_settings(A2_REQUIRED_FIELDS=['username'])
|
||||
def test_email_username_is_unique_double_registration(self):
|
||||
|
|
|
@ -25,11 +25,12 @@ from django.urls import reverse
|
|||
from httmock import HTTMock, remember_called, urlmatch
|
||||
|
||||
from authentic2 import models
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
||||
from authentic2.apps.journal.models import Event
|
||||
from authentic2.forms.profile import modelform_factory
|
||||
from authentic2.forms.registration import RegistrationCompletionForm
|
||||
from authentic2.models import Attribute, SMSCode, Token
|
||||
from authentic2.models import Attribute, AttributeValue, SMSCode, Token
|
||||
from authentic2.utils import misc as utils_misc
|
||||
from authentic2.validators import EmailValidator
|
||||
|
||||
|
@ -1098,6 +1099,188 @@ def test_phone_registration_connection_error(app, db, settings, freezer, caplog,
|
|||
)
|
||||
|
||||
|
||||
def test_phone_registration_number_already_existing_create(app, db, settings, phone_activated_authn):
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
# create duplicates
|
||||
for i in range(3):
|
||||
user = User.objects.create(
|
||||
first_name='John',
|
||||
last_name='Doe',
|
||||
email=f'john-{i}@example.com',
|
||||
username=f'john-{i}',
|
||||
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()
|
||||
location = resp.location.split('?')[0]
|
||||
resp = resp.follow()
|
||||
assert Token.objects.count() == 1
|
||||
|
||||
assert 'Existing accounts are associated with this phone number.' in resp.text
|
||||
# three existing accounts
|
||||
assert len(resp.pyquery('form')) == 3
|
||||
# the possibility to create a new one
|
||||
assert resp.pyquery('p a').text() == 'create a new account'
|
||||
resp = app.get(f'{location}?create')
|
||||
|
||||
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
|
||||
assert AttributeValue.objects.filter(content='+33612345678').count() == 4
|
||||
assert User.objects.filter(first_name='John', last_name='Doe').count() == 4
|
||||
|
||||
|
||||
def test_phone_registration_number_already_existing_select(app, db, settings, phone_activated_authn):
|
||||
settings.SMS_URL = 'https://foo.whatever.none/'
|
||||
|
||||
user_ids = []
|
||||
|
||||
# create duplicates
|
||||
for i in range(3):
|
||||
user = User.objects.create(
|
||||
first_name='John',
|
||||
last_name='Doe',
|
||||
email=f'john-{i}@example.com',
|
||||
username=f'john-{i}',
|
||||
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 'Existing accounts are associated with this phone number.' in resp.text
|
||||
# three existing accounts
|
||||
assert len(resp.pyquery('form')) == 3
|
||||
# the possibility to create a new one
|
||||
assert resp.pyquery('p a').text() == 'create a new account'
|
||||
|
||||
resp.forms[1].submit().follow()
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue