Forces validation on Account.set_password() and provides an Account.validate_password() method to validate passwords.

This commit is contained in:
Johnny 2018-09-20 20:37:48 +00:00
parent ddf01d1631
commit c8c9e831ee
4 changed files with 98 additions and 3 deletions

View file

@ -13,6 +13,8 @@ instead for most things).
import time
from django.conf import settings
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.utils import timezone
from evennia.typeclasses.models import TypeclassBase
from evennia.accounts.manager import AccountManager
@ -357,7 +359,66 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
puppet = property(__get_single_puppet)
# utility methods
@classmethod
def validate_password(cls, password, account=None):
"""
Checks the given password against the list of Django validators enabled
in the server.conf file.
Args:
password (str): Password to validate
Kwargs:
account (DefaultAccount, optional): Account object to validate the
password for. Optional, but Django includes some validators to
do things like making sure users aren't setting passwords to the
same value as their username. If left blank, these user-specific
checks are skipped.
Returns:
valid (bool): Whether or not the password passed validation
error (ValidationError, None): Any validation error(s) raised. Multiple
errors can be nested within a single object.
"""
valid = False
error = None
# Validation returns None on success; invert it and return a more sensible bool
try:
valid = not password_validation.validate_password(password, user=account)
except ValidationError as e:
error = e
return valid, error
def set_password(self, password, force=False):
"""
Applies the given password to the account if it passes validation checks.
Can be overridden by using the 'force' flag.
Args:
password (str): Password to set
Kwargs:
force (bool): Sets password without running validation checks.
Raises:
ValidationError
Returns:
None (None): Does not return a value.
"""
if not force:
# Run validation checks
valid, error = self.validate_password(password, account=self)
if error: raise error
super(DefaultAccount, self).set_password(password)
logger.log_info("Password succesfully changed for %s." % self)
self.at_password_change()
def delete(self, *args, **kwargs):
"""
Deletes the account permanently.
@ -714,6 +775,17 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
pass
def at_password_change(self, **kwargs):
"""
Called after a successful password set/modify.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
pass
def at_pre_login(self, **kwargs):
"""

View file

@ -57,6 +57,29 @@ class TestDefaultAccount(TestCase):
def setUp(self):
self.s1 = Session()
self.s1.sessid = 0
def test_password_validation(self):
"Check password validators deny bad passwords"
self.account = create.create_account("TestAccount%s" % randint(0, 9), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
self.assertFalse(self.account.validate_password(bad, account=self.account)[0])
"Check validators allow sufficiently complex passwords"
for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"):
self.assertTrue(self.account.validate_password(better, account=self.account)[0])
def test_password_change(self):
"Check password setting and validation is working as expected"
self.account = create.create_account("TestAccount%s" % randint(0, 9), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
from django.core.exceptions import ValidationError
# Try setting some bad passwords
for bad in ('', '#', 'TestAccount', 'password'):
self.assertRaises(ValidationError, self.account.set_password, bad)
# Try setting a better password (test for False; returns None on success)
self.assertFalse(self.account.set_password('Mxyzptlk'))
def test_puppet_object_no_object(self):
"Check puppet_object method called with no object param"
@ -157,4 +180,4 @@ class TestDefaultAccount(TestCase):
account.puppet_object(self.s1, obj)
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account."))
self.assertIsNone(obj.at_post_puppet.call_args)
self.assertIsNone(obj.at_post_puppet.call_args)

View file

@ -821,7 +821,7 @@ AUTH_PASSWORD_VALIDATORS = [
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{
'NAME': 'evennia.contrib.security.validators.EvenniaPasswordValidator',
'NAME': 'evennia.server.validators.EvenniaPasswordValidator',
},
]