mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 00:06:30 +01:00
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 time
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import password_validation
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth import authenticate, password_validation
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.accounts.manager import AccountManager
|
||||
from evennia.accounts.models import AccountDB
|
||||
|
|
@ -359,6 +360,74 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
puppet = property(__get_single_puppet)
|
||||
|
||||
# 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
|
||||
def normalize_username(cls, username):
|
||||
"""
|
||||
|
|
@ -379,6 +448,43 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
username = re.sub(r"\s+", " ", username).strip()
|
||||
|
||||
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
|
||||
def validate_password(cls, password, account=None):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from mock import Mock
|
||||
from random import randint
|
||||
from unittest import TestCase
|
||||
|
|
@ -59,6 +61,33 @@ class TestDefaultAccount(TestCase):
|
|||
self.s1 = Session()
|
||||
self.s1.puppet = None
|
||||
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):
|
||||
"Check password validators deny bad passwords"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,42 @@
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
from evennia.accounts.models import AccountDB
|
||||
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:
|
||||
|
||||
def __init__(self, regex=r"^[\w. @+\-',]+$", policy="Password should contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."):
|
||||
|
|
|
|||
|
|
@ -809,6 +809,28 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||
{'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.
|
||||
TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue