Adds username normalization/validation and authentication methods to Account class.
This commit is contained in:
parent
f407a90f45
commit
e990176a02
4 changed files with 194 additions and 2 deletions
|
|
@ -13,9 +13,10 @@ instead for most things).
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import password_validation
|
from django.contrib.auth import authenticate, password_validation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from evennia.typeclasses.models import TypeclassBase
|
from evennia.typeclasses.models import TypeclassBase
|
||||||
from evennia.accounts.manager import AccountManager
|
from evennia.accounts.manager import AccountManager
|
||||||
from evennia.accounts.models import AccountDB
|
from evennia.accounts.models import AccountDB
|
||||||
|
|
@ -359,6 +360,74 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
puppet = property(__get_single_puppet)
|
puppet = property(__get_single_puppet)
|
||||||
|
|
||||||
# utility methods
|
# utility methods
|
||||||
|
@classmethod
|
||||||
|
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
||||||
|
"""
|
||||||
|
Retrieves and instantiates validators for usernames.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
validator_config (list): List of dicts comprising the battery of
|
||||||
|
validators to apply to a username.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
validators (list): List of instantiated Validator objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
objs = []
|
||||||
|
for validator in validator_config:
|
||||||
|
try:
|
||||||
|
klass = import_string(validator['NAME'])
|
||||||
|
except ImportError:
|
||||||
|
msg = "The module in NAME could not be imported: %s. Check your AUTH_USERNAME_VALIDATORS setting."
|
||||||
|
raise ImproperlyConfigured(msg % validator['NAME'])
|
||||||
|
objs.append(klass(**validator.get('OPTIONS', {})))
|
||||||
|
return objs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate(cls, username, password, ip=None):
|
||||||
|
"""
|
||||||
|
Checks the given username/password against the database to see if the
|
||||||
|
credentials are valid.
|
||||||
|
|
||||||
|
Note that this simply checks credentials and returns a valid reference
|
||||||
|
to the user-- it does not log them in!
|
||||||
|
|
||||||
|
To finish the job:
|
||||||
|
After calling this from a Command, associate the account with a Session:
|
||||||
|
- session.sessionhandler.login(session, account)
|
||||||
|
|
||||||
|
...or after calling this from a View, associate it with an HttpRequest:
|
||||||
|
- django.contrib.auth.login(account, request)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username of account
|
||||||
|
password (str): Password of account
|
||||||
|
ip (str, optional): IP address of client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
account (DefaultAccount, None): Account whose credentials were
|
||||||
|
provided if not banned.
|
||||||
|
errors (list): Error messages of any failures.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
if ip: ip = str(ip)
|
||||||
|
|
||||||
|
# Authenticate and get Account object
|
||||||
|
account = authenticate(username=username, password=password)
|
||||||
|
if not account:
|
||||||
|
# User-facing message
|
||||||
|
errors.append('Username and/or password is incorrect.')
|
||||||
|
|
||||||
|
# System log message
|
||||||
|
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
||||||
|
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Account successfully authenticated
|
||||||
|
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
|
||||||
|
return account, errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_username(cls, username):
|
def normalize_username(cls, username):
|
||||||
"""
|
"""
|
||||||
|
|
@ -380,6 +449,43 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
|
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_username(cls, username):
|
||||||
|
"""
|
||||||
|
Checks the given username against the username validator associated with
|
||||||
|
Account objects, and also checks the database to make sure it is unique.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
valid (bool): Whether or not the password passed validation
|
||||||
|
errors (list): Error messages of any failures
|
||||||
|
|
||||||
|
"""
|
||||||
|
valid = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Make sure we're at least using the default validator
|
||||||
|
validators = cls.get_username_validators()
|
||||||
|
if not validators:
|
||||||
|
validators = [cls.username_validator]
|
||||||
|
|
||||||
|
# Try username against all enabled validators
|
||||||
|
for validator in validators:
|
||||||
|
try:
|
||||||
|
valid.append(not validator(username))
|
||||||
|
except ValidationError as e:
|
||||||
|
valid.append(False)
|
||||||
|
[errors.append(x) for x in e.messages]
|
||||||
|
|
||||||
|
# Disqualify if any check failed
|
||||||
|
if False in valid:
|
||||||
|
valid = False
|
||||||
|
else: valid = True
|
||||||
|
|
||||||
|
return valid, errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_password(cls, password, account=None):
|
def validate_password(cls, password, account=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
@ -60,6 +62,33 @@ class TestDefaultAccount(TestCase):
|
||||||
self.s1.puppet = None
|
self.s1.puppet = None
|
||||||
self.s1.sessid = 0
|
self.s1.sessid = 0
|
||||||
|
|
||||||
|
self.password = "testpassword"
|
||||||
|
self.account = create.create_account("TestAccount%s" % randint(100000, 999999), email="test@test.com", password=self.password, typeclass=DefaultAccount)
|
||||||
|
|
||||||
|
def test_authentication(self):
|
||||||
|
"Confirm Account authentication method is authenticating/denying users."
|
||||||
|
# Valid credentials
|
||||||
|
obj, errors = DefaultAccount.authenticate(self.account.name, self.password)
|
||||||
|
self.assertTrue(obj, 'Account did not authenticate given valid credentials.')
|
||||||
|
|
||||||
|
# Invalid credentials
|
||||||
|
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
|
||||||
|
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
|
||||||
|
|
||||||
|
def test_username_validation(self):
|
||||||
|
"Check username validators deny relevant usernames"
|
||||||
|
# Should not accept Unicode by default, lest users pick names like this
|
||||||
|
result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯')
|
||||||
|
self.assertFalse(result, "Validator allowed kanji in username.")
|
||||||
|
|
||||||
|
# Should not allow duplicate username
|
||||||
|
result, error = DefaultAccount.validate_username(self.account.name)
|
||||||
|
self.assertFalse(result, "Duplicate username should not have passed validation.")
|
||||||
|
|
||||||
|
# Should not allow username too short
|
||||||
|
result, error = DefaultAccount.validate_username('xx')
|
||||||
|
self.assertFalse(result, "2-character username passed validation.")
|
||||||
|
|
||||||
def test_password_validation(self):
|
def test_password_validation(self):
|
||||||
"Check password validators deny bad passwords"
|
"Check password validators deny bad passwords"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,42 @@
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
class EvenniaUsernameAvailabilityValidator:
|
||||||
|
"""
|
||||||
|
Checks to make sure a given username is not taken or otherwise reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, username):
|
||||||
|
"""
|
||||||
|
Validates a username to make sure it is not in use or reserved.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (None): None if password successfully validated,
|
||||||
|
raises ValidationError otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check guest list
|
||||||
|
if settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Sorry, that username is reserved.'),
|
||||||
|
code='evennia_username_reserved',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check database
|
||||||
|
exists = AccountDB.objects.filter(username__iexact=username).exists()
|
||||||
|
if exists:
|
||||||
|
raise ValidationError(
|
||||||
|
_('Sorry, that username is already taken.'),
|
||||||
|
code='evennia_username_taken',
|
||||||
|
)
|
||||||
|
|
||||||
class EvenniaPasswordValidator:
|
class EvenniaPasswordValidator:
|
||||||
|
|
||||||
def __init__(self, regex=r"^[\w. @+\-',]+$", policy="Password should contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."):
|
def __init__(self, regex=r"^[\w. @+\-',]+$", policy="Password should contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."):
|
||||||
|
|
|
||||||
|
|
@ -810,6 +810,28 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||||
{'NAME': 'evennia.server.validators.EvenniaPasswordValidator'}]
|
{'NAME': 'evennia.server.validators.EvenniaPasswordValidator'}]
|
||||||
|
|
||||||
|
# Username validation plugins
|
||||||
|
AUTH_USERNAME_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.validators.ASCIIUsernameValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.core.validators.MinLengthValidator',
|
||||||
|
'OPTIONS': {
|
||||||
|
'limit_value': 3,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.core.validators.MaxLengthValidator',
|
||||||
|
'OPTIONS': {
|
||||||
|
'limit_value': 30,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'evennia.server.validators.EvenniaUsernameAvailabilityValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
# Use a custom test runner that just tests Evennia-specific apps.
|
# Use a custom test runner that just tests Evennia-specific apps.
|
||||||
TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
|
TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue