mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Refs #21379 -- Normalized unicode username inputs
This commit is contained in:
		| @@ -4,6 +4,8 @@ not in INSTALLED_APPS. | ||||
| """ | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| import unicodedata | ||||
|  | ||||
| from django.contrib.auth import password_validation | ||||
| from django.contrib.auth.hashers import ( | ||||
|     check_password, is_password_usable, make_password, | ||||
| @@ -11,7 +13,7 @@ from django.contrib.auth.hashers import ( | ||||
| from django.db import models | ||||
| from django.utils.crypto import get_random_string, salted_hmac | ||||
| 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 _ | ||||
|  | ||||
|  | ||||
| @@ -31,6 +33,10 @@ class BaseUserManager(models.Manager): | ||||
|             email = '@'.join([email_name, domain_part.lower()]) | ||||
|         return email | ||||
|  | ||||
|     @classmethod | ||||
|     def normalize_username(cls, username): | ||||
|         return unicodedata.normalize('NFKC', force_text(username)) | ||||
|  | ||||
|     def make_random_password(self, length=10, | ||||
|                              allowed_chars='abcdefghjkmnpqrstuvwxyz' | ||||
|                                            'ABCDEFGHJKLMNPQRSTUVWXYZ' | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| import unicodedata | ||||
|  | ||||
| from django import forms | ||||
| from django.contrib.auth import ( | ||||
|     authenticate, get_user_model, password_validation, | ||||
| @@ -60,6 +62,11 @@ class ReadOnlyPasswordHashField(forms.Field): | ||||
|         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): | ||||
|     """ | ||||
|     A form that creates a user, with no privileges, from the given username and | ||||
| @@ -83,6 +90,7 @@ class UserCreationForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = ("username",) | ||||
|         field_classes = {'username': UsernameField} | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super(UserCreationForm, self).__init__(*args, **kwargs) | ||||
| @@ -121,6 +129,7 @@ class UserChangeForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = '__all__' | ||||
|         field_classes = {'username': UsernameField} | ||||
|  | ||||
|     def __init__(self, *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 | ||||
|     username/password logins. | ||||
|     """ | ||||
|     username = forms.CharField( | ||||
|     username = UsernameField( | ||||
|         max_length=254, | ||||
|         widget=forms.TextInput(attrs={'autofocus': ''}), | ||||
|     ) | ||||
|   | ||||
| @@ -145,6 +145,7 @@ class UserManager(BaseUserManager): | ||||
|         if not username: | ||||
|             raise ValueError('The given username must be set') | ||||
|         email = self.normalize_email(email) | ||||
|         username = self.normalize_username(username) | ||||
|         user = self.model(username=username, email=email, **extra_fields) | ||||
|         user.set_password(password) | ||||
|         user.save(using=self._db) | ||||
|   | ||||
| @@ -726,6 +726,14 @@ utility methods: | ||||
|         Normalizes email addresses by lowercasing the domain portion of the | ||||
|         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) | ||||
|  | ||||
|         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.models import AnonymousUser, User | ||||
| from django.core.exceptions import ImproperlyConfigured | ||||
| from django.db import IntegrityError | ||||
| from django.dispatch import receiver | ||||
| from django.test import TestCase, override_settings | ||||
| from django.test.signals import setting_changed | ||||
| @@ -60,6 +61,12 @@ class BasicTestCase(TestCase): | ||||
|     def test_unicode_username(self): | ||||
|         User.objects.create_user('jörg') | ||||
|         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): | ||||
|         deprecation_message = ( | ||||
|   | ||||
| @@ -3,6 +3,7 @@ from __future__ import unicode_literals | ||||
|  | ||||
| import datetime | ||||
| import re | ||||
| from unittest import skipIf | ||||
|  | ||||
| from django import forms | ||||
| from django.contrib.auth.forms import ( | ||||
| @@ -118,6 +119,28 @@ class UserCreationFormTest(TestDataMixin, TestCase): | ||||
|         else: | ||||
|             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=[ | ||||
|         {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, | ||||
|         {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user