Commit 9935f97c authored by Claude Paroz's avatar Claude Paroz
Browse files

Refs #21379 -- Normalized unicode username inputs

parent 526575c6
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -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'
+10 −1
Original line number Diff line number Diff line
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': ''}),
    )
+1 −0
Original line number Diff line number Diff line
@@ -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)
+8 −0
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
@@ -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 = (
Loading