Fixed #21379 -- Created auth-specific username validators
Thanks Tim Graham for the review.
This commit is contained in:
parent
2265ff3710
commit
526575c641
|
@ -2,9 +2,9 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
from django.core import validators
|
from django.contrib.auth import validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.utils import timezone
|
from django.utils import six, timezone
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -66,7 +66,9 @@ class Migration(migrations.Migration):
|
||||||
('username', models.CharField(
|
('username', models.CharField(
|
||||||
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True,
|
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True,
|
||||||
max_length=30, verbose_name='username',
|
max_length=30, verbose_name='username',
|
||||||
validators=[validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username.', 'invalid')]
|
validators=[
|
||||||
|
validators.UnicodeUsernameValidator() if six.PY3 else validators.ASCIIUsernameValidator()
|
||||||
|
],
|
||||||
)),
|
)),
|
||||||
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
|
('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
|
||||||
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
|
('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.core.validators
|
from django.contrib.auth import validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -18,11 +19,7 @@ class Migration(migrations.Migration):
|
||||||
name='username',
|
name='username',
|
||||||
field=models.CharField(
|
field=models.CharField(
|
||||||
error_messages={'unique': 'A user with that username already exists.'}, max_length=30,
|
error_messages={'unique': 'A user with that username already exists.'}, max_length=30,
|
||||||
validators=[django.core.validators.RegexValidator(
|
validators=[validators.UnicodeUsernameValidator() if six.PY3 else validators.ASCIIUsernameValidator()],
|
||||||
'^[\\w.@+-]+$',
|
|
||||||
'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.',
|
|
||||||
'invalid'
|
|
||||||
)],
|
|
||||||
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
||||||
unique=True, verbose_name='username'
|
unique=True, verbose_name='username'
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.core.validators
|
from django.contrib.auth import validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -20,12 +21,7 @@ class Migration(migrations.Migration):
|
||||||
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
||||||
max_length=30,
|
max_length=30,
|
||||||
unique=True,
|
unique=True,
|
||||||
validators=[
|
validators=[validators.UnicodeUsernameValidator() if six.PY3 else validators.ASCIIUsernameValidator()],
|
||||||
django.core.validators.RegexValidator(
|
|
||||||
'^[\\w.@+-]+$', 'Enter a valid username. '
|
|
||||||
'This value may contain only letters, numbers and @/./+/-/_ characters.'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
verbose_name='username',
|
verbose_name='username',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.core.validators
|
from django.contrib.auth import validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -20,12 +21,7 @@ class Migration(migrations.Migration):
|
||||||
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
||||||
max_length=150,
|
max_length=150,
|
||||||
unique=True,
|
unique=True,
|
||||||
validators=[
|
validators=[validators.UnicodeUsernameValidator() if six.PY3 else validators.ASCIIUsernameValidator()],
|
||||||
django.core.validators.RegexValidator(
|
|
||||||
'^[\\w.@+-]+$', 'Enter a valid username. '
|
|
||||||
'This value may contain only letters, numbers and @/./+/-/_ characters.'
|
|
||||||
),
|
|
||||||
],
|
|
||||||
verbose_name='username',
|
verbose_name='username',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,7 +4,6 @@ from django.contrib import auth
|
||||||
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
||||||
from django.contrib.auth.signals import user_logged_in
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core import validators
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -14,6 +13,8 @@ from django.utils.deprecation import CallableFalse, CallableTrue
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .validators import ASCIIUsernameValidator, UnicodeUsernameValidator
|
||||||
|
|
||||||
|
|
||||||
def update_last_login(sender, user, **kwargs):
|
def update_last_login(sender, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -302,18 +303,14 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
|
||||||
|
|
||||||
Username and password are required. Other fields are optional.
|
Username and password are required. Other fields are optional.
|
||||||
"""
|
"""
|
||||||
|
username_validator = UnicodeUsernameValidator() if six.PY3 else ASCIIUsernameValidator()
|
||||||
|
|
||||||
username = models.CharField(
|
username = models.CharField(
|
||||||
_('username'),
|
_('username'),
|
||||||
max_length=150,
|
max_length=150,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
|
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
|
||||||
validators=[
|
validators=[username_validator],
|
||||||
validators.RegexValidator(
|
|
||||||
r'^[\w.@+-]+$',
|
|
||||||
_('Enter a valid username. This value may contain only '
|
|
||||||
'letters, numbers ' 'and @/./+/-/_ characters.')
|
|
||||||
),
|
|
||||||
],
|
|
||||||
error_messages={
|
error_messages={
|
||||||
'unique': _("A user with that username already exists."),
|
'unique': _("A user with that username already exists."),
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.core import validators
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class ASCIIUsernameValidator(validators.RegexValidator):
|
||||||
|
regex = r'^[\w.@+-]+$'
|
||||||
|
message = _(
|
||||||
|
'Enter a valid username. This value may contain only English letters, '
|
||||||
|
'numbers, and @/./+/-/_ characters.'
|
||||||
|
)
|
||||||
|
flags = re.ASCII if six.PY3 else 0
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class UnicodeUsernameValidator(validators.RegexValidator):
|
||||||
|
regex = r'^[\w.@+-]+$'
|
||||||
|
message = _(
|
||||||
|
'Enter a valid username. This value may contain only letters, '
|
||||||
|
'numbers, and @/./+/-/_ characters.'
|
||||||
|
)
|
||||||
|
flags = re.UNICODE if six.PY2 else 0
|
|
@ -32,6 +32,15 @@ Fields
|
||||||
``max_length=191`` because MySQL can only create unique indexes with
|
``max_length=191`` because MySQL can only create unique indexes with
|
||||||
191 characters in that case by default.
|
191 characters in that case by default.
|
||||||
|
|
||||||
|
.. admonition:: Usernames and Unicode
|
||||||
|
|
||||||
|
Django originally accepted only ASCII letters in usernames.
|
||||||
|
Although it wasn't a deliberate choice, Unicode characters have
|
||||||
|
always been accepted when using Python 3. Django 1.10 officially
|
||||||
|
added Unicode support in usernames, keeping the ASCII-only behavior
|
||||||
|
on Python 2, with the option to customize the behavior using
|
||||||
|
:attr:`.User.username_validator`.
|
||||||
|
|
||||||
.. versionchanged:: 1.10
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
The ``max_length`` increased from 30 to 150 characters.
|
The ``max_length`` increased from 30 to 150 characters.
|
||||||
|
@ -146,6 +155,27 @@ Attributes
|
||||||
In older versions, this was a method. Backwards-compatibility
|
In older versions, this was a method. Backwards-compatibility
|
||||||
support for using it as a method will be removed in Django 2.0.
|
support for using it as a method will be removed in Django 2.0.
|
||||||
|
|
||||||
|
.. attribute:: username_validator
|
||||||
|
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
Points to a validator instance used to validate usernames. Defaults to
|
||||||
|
:class:`validators.UnicodeUsernameValidator` on Python 3 and
|
||||||
|
:class:`validators.ASCIIUsernameValidator` on Python 2.
|
||||||
|
|
||||||
|
To change the default username validator, you can subclass the ``User``
|
||||||
|
model and set this attribute to a different validator instance. For
|
||||||
|
example, to use ASCII usernames on Python 3::
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth.validators import ASCIIUsernameValidator
|
||||||
|
|
||||||
|
class CustomUser(User):
|
||||||
|
username_validator = ASCIIUsernameValidator()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
proxy = True # If no new field is added.
|
||||||
|
|
||||||
Methods
|
Methods
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -285,7 +315,6 @@ Manager methods
|
||||||
Same as :meth:`create_user`, but sets :attr:`~models.User.is_staff` and
|
Same as :meth:`create_user`, but sets :attr:`~models.User.is_staff` and
|
||||||
:attr:`~models.User.is_superuser` to ``True``.
|
:attr:`~models.User.is_superuser` to ``True``.
|
||||||
|
|
||||||
|
|
||||||
``AnonymousUser`` object
|
``AnonymousUser`` object
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
@ -378,6 +407,25 @@ Fields
|
||||||
group.permissions.remove(permission, permission, ...)
|
group.permissions.remove(permission, permission, ...)
|
||||||
group.permissions.clear()
|
group.permissions.clear()
|
||||||
|
|
||||||
|
Validators
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. class:: validators.ASCIIUsernameValidator
|
||||||
|
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
A field validator allowing only ASCII letters, in addition to ``@``, ``.``,
|
||||||
|
``+``, ``-``, and ``_``. The default validator for ``User.username`` on
|
||||||
|
Python 2.
|
||||||
|
|
||||||
|
.. class:: validators.UnicodeUsernameValidator
|
||||||
|
|
||||||
|
.. versionadded:: 1.10
|
||||||
|
|
||||||
|
A field validator allowing Unicode letters, in addition to ``@``, ``.``,
|
||||||
|
``+``, ``-``, and ``_``. The default validator for ``User.username`` on
|
||||||
|
Python 3.
|
||||||
|
|
||||||
.. _topics-auth-signals:
|
.. _topics-auth-signals:
|
||||||
|
|
||||||
Login and logout signals
|
Login and logout signals
|
||||||
|
|
|
@ -37,6 +37,22 @@ It also now includes trigram support, using the :lookup:`trigram_similar`
|
||||||
lookup, and the :class:`~django.contrib.postgres.search.TrigramSimilarity` and
|
lookup, and the :class:`~django.contrib.postgres.search.TrigramSimilarity` and
|
||||||
:class:`~django.contrib.postgres.search.TrigramDistance` expressions.
|
:class:`~django.contrib.postgres.search.TrigramDistance` expressions.
|
||||||
|
|
||||||
|
Official support for Unicode usernames
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
The :class:`~django.contrib.auth.models.User` model in ``django.contrib.auth``
|
||||||
|
originally only accepted ASCII letters in usernames. Although it wasn't a
|
||||||
|
deliberate choice, Unicode characters have always been accepted when using
|
||||||
|
Python 3.
|
||||||
|
|
||||||
|
The username validator now explicitly accepts Unicode letters by
|
||||||
|
default on Python 3 only. This default behavior can be overridden by changing
|
||||||
|
the :attr:`~django.contrib.auth.models.User.username_validator` attribute of
|
||||||
|
the ``User`` model, or to any proxy of that model, using either
|
||||||
|
:class:`~django.contrib.auth.validators.ASCIIUsernameValidator` or
|
||||||
|
:class:`~django.contrib.auth.validators.UnicodeUsernameValidator`. Custom user
|
||||||
|
models may also use those validators.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
@ -56,6 +57,10 @@ class BasicTestCase(TestCase):
|
||||||
u2 = User.objects.create_user('testuser2', 'test2@example.com')
|
u2 = User.objects.create_user('testuser2', 'test2@example.com')
|
||||||
self.assertFalse(u2.has_usable_password())
|
self.assertFalse(u2.has_usable_password())
|
||||||
|
|
||||||
|
def test_unicode_username(self):
|
||||||
|
User.objects.create_user('jörg')
|
||||||
|
User.objects.create_user('Григорий')
|
||||||
|
|
||||||
def test_is_anonymous_authenticated_method_deprecation(self):
|
def test_is_anonymous_authenticated_method_deprecation(self):
|
||||||
deprecation_message = (
|
deprecation_message = (
|
||||||
'Using user.is_authenticated() and user.is_anonymous() as a '
|
'Using user.is_authenticated() and user.is_anonymous() as a '
|
||||||
|
|
|
@ -16,7 +16,7 @@ from django.core import mail
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.forms.fields import CharField, Field
|
from django.forms.fields import CharField, Field
|
||||||
from django.test import SimpleTestCase, TestCase, mock, override_settings
|
from django.test import SimpleTestCase, TestCase, mock, override_settings
|
||||||
from django.utils import translation
|
from django.utils import six, translation
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
@ -104,6 +104,20 @@ class UserCreationFormTest(TestDataMixin, TestCase):
|
||||||
self.assertEqual(password_changed.call_count, 1)
|
self.assertEqual(password_changed.call_count, 1)
|
||||||
self.assertEqual(repr(u), '<User: jsmith@example.com>')
|
self.assertEqual(repr(u), '<User: jsmith@example.com>')
|
||||||
|
|
||||||
|
def test_unicode_username(self):
|
||||||
|
data = {
|
||||||
|
'username': '宝',
|
||||||
|
'password1': 'test123',
|
||||||
|
'password2': 'test123',
|
||||||
|
}
|
||||||
|
form = UserCreationForm(data)
|
||||||
|
if six.PY3:
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
u = form.save()
|
||||||
|
self.assertEqual(u.username, '宝')
|
||||||
|
else:
|
||||||
|
self.assertFalse(form.is_valid())
|
||||||
|
|
||||||
@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': {
|
||||||
|
@ -254,6 +268,16 @@ class AuthenticationFormTest(TestDataMixin, TestCase):
|
||||||
self.assertTrue(form.is_valid())
|
self.assertTrue(form.is_valid())
|
||||||
self.assertEqual(form.non_field_errors(), [])
|
self.assertEqual(form.non_field_errors(), [])
|
||||||
|
|
||||||
|
def test_unicode_username(self):
|
||||||
|
User.objects.create_user(username='Σαρα', password='pwd')
|
||||||
|
data = {
|
||||||
|
'username': 'Σαρα',
|
||||||
|
'password': 'pwd',
|
||||||
|
}
|
||||||
|
form = AuthenticationForm(None, data)
|
||||||
|
self.assertTrue(form.is_valid())
|
||||||
|
self.assertEqual(form.non_field_errors(), [])
|
||||||
|
|
||||||
def test_username_field_label(self):
|
def test_username_field_label(self):
|
||||||
|
|
||||||
class CustomAuthenticationForm(AuthenticationForm):
|
class CustomAuthenticationForm(AuthenticationForm):
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.contrib.auth import validators
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
CommonPasswordValidator, MinimumLengthValidator, NumericPasswordValidator,
|
CommonPasswordValidator, MinimumLengthValidator, NumericPasswordValidator,
|
||||||
|
@ -174,3 +176,29 @@ class NumericPasswordValidatorTest(TestCase):
|
||||||
NumericPasswordValidator().get_help_text(),
|
NumericPasswordValidator().get_help_text(),
|
||||||
"Your password can't be entirely numeric."
|
"Your password can't be entirely numeric."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UsernameValidatorsTests(TestCase):
|
||||||
|
def test_unicode_validator(self):
|
||||||
|
valid_usernames = ['joe', 'René', 'ᴮᴵᴳᴮᴵᴿᴰ', 'أحمد']
|
||||||
|
invalid_usernames = [
|
||||||
|
"o'connell", "عبد ال",
|
||||||
|
"zerowidth\u200Bspace", "nonbreaking\u00A0space",
|
||||||
|
"en\u2013dash",
|
||||||
|
]
|
||||||
|
v = validators.UnicodeUsernameValidator()
|
||||||
|
for valid in valid_usernames:
|
||||||
|
v(valid)
|
||||||
|
for invalid in invalid_usernames:
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v(invalid)
|
||||||
|
|
||||||
|
def test_ascii_validator(self):
|
||||||
|
valid_usernames = ['glenn', 'GLEnN', 'jean-marc']
|
||||||
|
invalid_usernames = ["o'connell", 'Éric', 'jean marc', "أحمد"]
|
||||||
|
v = validators.ASCIIUsernameValidator()
|
||||||
|
for valid in valid_usernames:
|
||||||
|
v(valid)
|
||||||
|
for invalid in invalid_usernames:
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v(invalid)
|
||||||
|
|
Loading…
Reference in New Issue