Refs #21379 -- Normalized unicode username inputs
This commit is contained in:
parent
526575c641
commit
9935f97cd2
|
@ -4,6 +4,8 @@ not in INSTALLED_APPS.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
from django.contrib.auth import password_validation
|
from django.contrib.auth import password_validation
|
||||||
from django.contrib.auth.hashers import (
|
from django.contrib.auth.hashers import (
|
||||||
check_password, is_password_usable, make_password,
|
check_password, is_password_usable, make_password,
|
||||||
|
@ -11,7 +13,7 @@ from django.contrib.auth.hashers import (
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.crypto import get_random_string, salted_hmac
|
from django.utils.crypto import get_random_string, salted_hmac
|
||||||
from django.utils.deprecation import CallableFalse, CallableTrue
|
from django.utils.deprecation import CallableFalse, CallableTrue
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +33,10 @@ class BaseUserManager(models.Manager):
|
||||||
email = '@'.join([email_name, domain_part.lower()])
|
email = '@'.join([email_name, domain_part.lower()])
|
||||||
return email
|
return email
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def normalize_username(cls, username):
|
||||||
|
return unicodedata.normalize('NFKC', force_text(username))
|
||||||
|
|
||||||
def make_random_password(self, length=10,
|
def make_random_password(self, length=10,
|
||||||
allowed_chars='abcdefghjkmnpqrstuvwxyz'
|
allowed_chars='abcdefghjkmnpqrstuvwxyz'
|
||||||
'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth import (
|
from django.contrib.auth import (
|
||||||
authenticate, get_user_model, password_validation,
|
authenticate, get_user_model, password_validation,
|
||||||
|
@ -60,6 +62,11 @@ class ReadOnlyPasswordHashField(forms.Field):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class UsernameField(forms.CharField):
|
||||||
|
def to_python(self, value):
|
||||||
|
return unicodedata.normalize('NFKC', super(UsernameField, self).to_python(value))
|
||||||
|
|
||||||
|
|
||||||
class UserCreationForm(forms.ModelForm):
|
class UserCreationForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
A form that creates a user, with no privileges, from the given username and
|
A form that creates a user, with no privileges, from the given username and
|
||||||
|
@ -83,6 +90,7 @@ class UserCreationForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ("username",)
|
fields = ("username",)
|
||||||
|
field_classes = {'username': UsernameField}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(UserCreationForm, self).__init__(*args, **kwargs)
|
super(UserCreationForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -121,6 +129,7 @@ class UserChangeForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
field_classes = {'username': UsernameField}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(UserChangeForm, self).__init__(*args, **kwargs)
|
super(UserChangeForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -140,7 +149,7 @@ class AuthenticationForm(forms.Form):
|
||||||
Base class for authenticating users. Extend this to get a form that accepts
|
Base class for authenticating users. Extend this to get a form that accepts
|
||||||
username/password logins.
|
username/password logins.
|
||||||
"""
|
"""
|
||||||
username = forms.CharField(
|
username = UsernameField(
|
||||||
max_length=254,
|
max_length=254,
|
||||||
widget=forms.TextInput(attrs={'autofocus': ''}),
|
widget=forms.TextInput(attrs={'autofocus': ''}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -145,6 +145,7 @@ class UserManager(BaseUserManager):
|
||||||
if not username:
|
if not username:
|
||||||
raise ValueError('The given username must be set')
|
raise ValueError('The given username must be set')
|
||||||
email = self.normalize_email(email)
|
email = self.normalize_email(email)
|
||||||
|
username = self.normalize_username(username)
|
||||||
user = self.model(username=username, email=email, **extra_fields)
|
user = self.model(username=username, email=email, **extra_fields)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save(using=self._db)
|
user.save(using=self._db)
|
||||||
|
|
|
@ -726,6 +726,14 @@ utility methods:
|
||||||
Normalizes email addresses by lowercasing the domain portion of the
|
Normalizes email addresses by lowercasing the domain portion of the
|
||||||
email address.
|
email address.
|
||||||
|
|
||||||
|
.. classmethod:: models.BaseUserManager.normalize_username(email)
|
||||||
|
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
Applies NFKC Unicode normalization to usernames so that visually
|
||||||
|
identical characters with different Unicode code points are considered
|
||||||
|
identical.
|
||||||
|
|
||||||
.. method:: models.BaseUserManager.get_by_natural_key(username)
|
.. method:: models.BaseUserManager.get_by_natural_key(username)
|
||||||
|
|
||||||
Retrieves a user instance using the contents of the field
|
Retrieves a user instance using the contents of the field
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.apps import apps
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import AnonymousUser, User
|
from django.contrib.auth.models import AnonymousUser, User
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.test.signals import setting_changed
|
from django.test.signals import setting_changed
|
||||||
|
@ -60,6 +61,12 @@ class BasicTestCase(TestCase):
|
||||||
def test_unicode_username(self):
|
def test_unicode_username(self):
|
||||||
User.objects.create_user('jörg')
|
User.objects.create_user('jörg')
|
||||||
User.objects.create_user('Григорий')
|
User.objects.create_user('Григорий')
|
||||||
|
# Two equivalent unicode normalized usernames should be duplicates
|
||||||
|
omega_username = 'iamtheΩ' # U+03A9 GREEK CAPITAL LETTER OMEGA
|
||||||
|
ohm_username = 'iamtheΩ' # U+2126 OHM SIGN
|
||||||
|
User.objects.create_user(ohm_username)
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
User.objects.create_user(omega_username)
|
||||||
|
|
||||||
def test_is_anonymous_authenticated_method_deprecation(self):
|
def test_is_anonymous_authenticated_method_deprecation(self):
|
||||||
deprecation_message = (
|
deprecation_message = (
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
from unittest import skipIf
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import (
|
from django.contrib.auth.forms import (
|
||||||
|
@ -118,6 +119,28 @@ class UserCreationFormTest(TestDataMixin, TestCase):
|
||||||
else:
|
else:
|
||||||
self.assertFalse(form.is_valid())
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
|
@skipIf(six.PY2, "Python 2 doesn't support unicode usernames by default.")
|
||||||
|
def test_duplicate_normalized_unicode(self):
|
||||||
|
"""
|
||||||
|
To prevent almost identical usernames, visually identical but differing
|
||||||
|
by their unicode code points only, Unicode NFKC normalization should
|
||||||
|
make appear them equal to Django.
|
||||||
|
"""
|
||||||
|
omega_username = 'iamtheΩ' # U+03A9 GREEK CAPITAL LETTER OMEGA
|
||||||
|
ohm_username = 'iamtheΩ' # U+2126 OHM SIGN
|
||||||
|
self.assertNotEqual(omega_username, ohm_username)
|
||||||
|
User.objects.create_user(username=omega_username, password='pwd')
|
||||||
|
data = {
|
||||||
|
'username': ohm_username,
|
||||||
|
'password1': 'pwd2',
|
||||||
|
'password2': 'pwd2',
|
||||||
|
}
|
||||||
|
form = UserCreationForm(data)
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
self.assertEqual(
|
||||||
|
form.errors['username'], ["A user with that username already exists."]
|
||||||
|
)
|
||||||
|
|
||||||
@override_settings(AUTH_PASSWORD_VALIDATORS=[
|
@override_settings(AUTH_PASSWORD_VALIDATORS=[
|
||||||
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
||||||
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {
|
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {
|
||||||
|
|
Loading…
Reference in New Issue