From 21d66ab625ac4e91cbf3d3306dd7b585ebef2f38 Mon Sep 17 00:00:00 2001 From: Johnny Date: Tue, 2 Oct 2018 20:23:23 +0000 Subject: [PATCH] Moves account creation logic from Commands module to Account class. --- evennia/accounts/accounts.py | 269 ++++++++++++++++++++++++- evennia/accounts/tests.py | 37 +++- evennia/commands/default/unloggedin.py | 222 +++++--------------- evennia/server/validators.py | 2 +- 4 files changed, 354 insertions(+), 176 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index a6a29fab72..6bd812b6fa 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -23,8 +23,9 @@ from evennia.accounts.models import AccountDB from evennia.objects.models import ObjectDB from evennia.comms.models import ChannelDB from evennia.commands import cmdhandler +from evennia.server.models import ServerConfig from evennia.server.throttle import Throttle -from evennia.utils import logger +from evennia.utils import create, logger from evennia.utils.utils import (lazy_property, to_str, make_iter, to_unicode, is_iter, variable_from_module) @@ -34,6 +35,7 @@ from evennia.commands.cmdsethandler import CmdSetHandler from django.utils.translation import ugettext as _ from future.utils import with_metaclass +from random import getrandbits __all__ = ("DefaultAccount",) @@ -364,6 +366,31 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): puppet = property(__get_single_puppet) # utility methods + @classmethod + def is_banned(cls, **kwargs): + """ + Checks if a given username or IP is banned. + + Kwargs: + ip (str, optional): IP address. + username (str, optional): Username. + + Returns: + is_banned (bool): Whether either is banned or not. + + """ + + ip = kwargs.get('ip', '').strip() + username = kwargs.get('username', '').lower().strip() + + # Check IP and/or name bans + bans = ServerConfig.objects.conf("server_bans") + if bans and (any(tup[0] == username for tup in bans if username) or + any(tup[2].match(ip) for tup in bans if ip and tup[2])): + return True + + return False + @classmethod def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])): """ @@ -386,9 +413,85 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): raise ImproperlyConfigured(msg % validator['NAME']) objs.append(klass(**validator.get('OPTIONS', {}))) return objs + + @classmethod + def authenticate_guest(cls, **kwargs): + """ + Gets or creates a Guest account object. + + Kwargs: + ip (str, optional): IP address of requestor; used for ban checking, + throttling and logging + + """ + errors = [] + account = None + username = None + ip = kwargs.get('ip', '').strip() + + # check if guests are enabled. + if not settings.GUEST_ENABLED: + errors.append('Guest accounts are not enabled on this server.') + return None, errors + + # See if authentication is currently being throttled + if ip and LOGIN_THROTTLE.check(ip): + errors.append('Too many login failures; please try again in a few minutes.') + + # With throttle active, do not log continued hits-- it is a + # waste of storage and can be abused to make your logs harder to + # read and/or fill up your disk. + return None, errors + + # check if IP banned + if ip and cls.is_banned(ip=ip): + errors.append("|rYou have been banned and cannot continue from here." \ + "\nIf you feel this ban is in error, please email an admin.|x") + logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % ('guest', ip)) + LOGIN_THROTTLE.update(ip, 'Too many sightings of banned IP.') + return None, errors + + try: + # Find an available guest name. + for name in settings.GUEST_LIST: + if not AccountDB.objects.filter(username__iexact=name).count(): + username = name + break + if not username: + errors.append("All guest accounts are in use. Please try again later.") + if ip: LOGIN_THROTTLE.update(ip, 'Too many requests for Guest access.') + return None, errors + else: + # build a new account with the found guest username + password = "%016x" % getrandbits(64) + home = ObjectDB.objects.get_id(settings.GUEST_HOME) + permissions = settings.PERMISSION_GUEST_DEFAULT + character_typeclass = settings.BASE_CHARACTER_TYPECLASS + account_typeclass = settings.BASE_GUEST_TYPECLASS + account, errs = cls.create( + guest=True, + username=username, + password=password, + permissions=permissions, + account_typeclass=account_typeclass, + character_typeclass=character_typeclass, + ip=ip, + ) + errors.extend(errs) + return account, errors + + except Exception: + # We are in the middle between logged in and -not, so we have + # to handle tracebacks ourselves at this point. If we don't, + # we won't see any errors at all. + errors.append("An error occurred. Please e-mail an admin if the problem persists.") + logger.log_trace() + return None, errors + + return account, errors @classmethod - def authenticate(cls, username, password, ip=None): + def authenticate(cls, username, password, ip='', **kwargs): """ Checks the given username/password against the database to see if the credentials are valid. @@ -408,6 +511,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): password (str): Password of account ip (str, optional): IP address of client + Kwargs: + session (Session, optional): Session requesting authentication + Returns: account (DefaultAccount, None): Account whose credentials were provided if not banned. @@ -423,7 +529,17 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): # With throttle active, do not log continued hits-- it is a # waste of storage and can be abused to make your logs harder to - # read and fill up your disk. + # read and/or fill up your disk. + return None, errors + + # Check IP and/or name bans + banned = cls.is_banned(username=username, ip=ip) + if banned: + # this is a banned IP or name! + errors.append("|rYou have been banned and cannot continue from here." \ + "\nIf you feel this ban is in error, please email an admin.|x") + logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip)) + LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.') return None, errors # Authenticate and get Account object @@ -436,7 +552,14 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip)) # Update throttle - if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures') + if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures.') + + # Try to call post-failure hook + session = kwargs.get('session', None) + if session: + account = AccountDB.objects.get_account_from_name(username) + if account: + account.at_failed_login(session) return None, errors @@ -493,7 +616,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): valid.append(not validator(username)) except ValidationError as e: valid.append(False) - [errors.append(x) for x in e.messages] + errors.extend(e.messages) # Disqualify if any check failed if False in valid: @@ -561,6 +684,142 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): super(DefaultAccount, self).set_password(password) logger.log_sec("Password successfully changed for %s." % self) self.at_password_change() + + @classmethod + def create(cls, *args, **kwargs): + """ + Creates an Account (or Account/Character pair for MM<2) with default + (or overridden) permissions and having joined them to the appropriate + default channels. + + Kwargs: + username (str): Username of Account owner + password (str): Password of Account owner + email (str, optional): Email address of Account owner + ip (str, optional): IP address of requesting connection + guest (bool, optional): Whether or not this is to be a Guest account + + permissions (str, optional): Default permissions for the Account + account_typeclass (str, optional): Typeclass to use for new Account + character_typeclass (str, optional): Typeclass to use for new char + when applicable. + + Returns: + account (Account): Account if successfully created; None if not + errors (list): List of error messages in string form + + """ + + account = None + errors = [] + + username = kwargs.get('username') + password = kwargs.get('password') + email = kwargs.get('email', '').strip() + guest = kwargs.get('guest', False) + + permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT) + account_typeclass = kwargs.get('account_typeclass', settings.BASE_ACCOUNT_TYPECLASS) + character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS) + + ip = kwargs.get('ip', '') + if ip and CREATION_THROTTLE.check(ip): + errors.append("You are creating too many accounts. Please log into an existing account.") + return None, errors + + # Normalize username + username = cls.normalize_username(username) + + # Validate username + if not guest: + valid, errs = cls.validate_username(username) + if not valid: + # this echoes the restrictions made by django's auth + # module (except not allowing spaces, for convenience of + # logging in). + errors.extend(errs) + return None, errors + + # Validate password + # Have to create a dummy Account object to check username similarity + valid, errs = cls.validate_password(password, account=cls(username=username)) + if not valid: + errors.extend(errs) + return None, errors + + # Check IP and/or name bans + banned = cls.is_banned(username=username, ip=ip) + if banned: + # this is a banned IP or name! + string = "|rYou have been banned and cannot continue from here." \ + "\nIf you feel this ban is in error, please email an admin.|x" + errors.append(string) + return None, errors + + # everything's ok. Create the new account account. + try: + try: + account = create.create_account(username, email, password, permissions=permissions, typeclass=account_typeclass) + logger.log_sec('Account Created: %s (IP: %s).' % (account, ip)) + + except Exception as e: + errors.append("There was an error creating the Account. If this problem persists, contact an admin.") + logger.log_trace() + return None, errors + + # This needs to be set so the engine knows this account is + # logging in for the first time. (so it knows to call the right + # hooks during login later) + account.db.FIRST_LOGIN = True + + # Record IP address of creation, if available + if ip: account.db.creator_ip = ip + + # join the new account to the public channel + pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"]) + if not pchannel or not pchannel.connect(account): + string = "New account '%s' could not connect to public channel!" % account.key + errors.append(string) + logger.log_err(string) + + if account: + if settings.MULTISESSION_MODE < 2: + default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) + + try: + character = create.create_object(character_typeclass, key=account.key, home=default_home, permissions=permissions) + + # set playable character list + account.db._playable_characters.append(character) + + # allow only the character itself and the account to puppet this character (and Developers). + character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" % + (character.id, account.id)) + + # If no description is set, set a default description + if not character.db.desc: + character.db.desc = "This is a character." + # We need to set this to have @ic auto-connect to this character + account.db._last_puppet = character + + # Record creator id and creation IP + if ip: character.db.creator_ip = ip + character.db.creator_id = account.id + + except Exception as e: + errors.append("There was an error creating a Character. If this problem persists, contact an admin.") + logger.log_trace() + + except Exception: + # We are in the middle between logged in and -not, so we have + # to handle tracebacks ourselves at this point. If we don't, + # we won't see any errors at all. + errors.append("An error occurred. Please e-mail an admin if the problem persists.") + logger.log_trace() + + # Update the throttle to indicate a new account was created from this IP + if ip and not guest: CREATION_THROTTLE.update(ip, 'Too many accounts being created.') + return account, errors def delete(self, *args, **kwargs): """ diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index f1a9f16c28..72f13bbb3a 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -75,6 +75,41 @@ class TestDefaultAccount(TestCase): obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy') self.assertFalse(obj, 'Account authenticated using invalid credentials.') + def test_create(self): + "Confirm Account creation is working as expected." + # Create a normal account + account, errors = DefaultAccount.create(username='ziggy', password='stardust11') + self.assertTrue(account, 'New account should have been created.') + + # Try creating a duplicate account + account, errors = DefaultAccount.create(username='Ziggy', password='starman11') + self.assertFalse(account, 'Duplicate account name should not have been allowed.') + + # Guest account should not be permitted + account, errors = DefaultAccount.authenticate_guest() + self.assertFalse(account, 'Guest account was created despite being disabled.') + + settings.GUEST_ENABLED = True + settings.GUEST_LIST = ['bruce_wayne'] + + # Create a guest account + account, errors = DefaultAccount.authenticate_guest() + self.assertTrue(account, 'Guest account should have been created.') + + # Create a second guest account + account, errors = DefaultAccount.authenticate_guest() + self.assertFalse(account, 'Two guest accounts were created despite a single entry on the guest list!') + + settings.GUEST_ENABLED = False + + def test_throttle(self): + "Confirm throttle activates on too many failures." + for x in xrange(20): + obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy', ip='12.24.36.48') + self.assertFalse(obj, 'Authentication was provided a bogus password; this should NOT have returned an account!') + + self.assertTrue('too many login failures' in errors[-1].lower(), 'Failed logins should have been throttled.') + def test_username_validation(self): "Check username validators deny relevant usernames" # Should not accept Unicode by default, lest users pick names like this @@ -92,7 +127,7 @@ class TestDefaultAccount(TestCase): def test_password_validation(self): "Check password validators deny bad passwords" - self.account = create.create_account("TestAccount%s" % randint(0, 9), + self.account = create.create_account("TestAccount%s" % randint(100000, 999999), 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]) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index d77d7ee2b9..7cf8b40c88 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -4,10 +4,9 @@ Commands that are available from the connect screen. import re import time import datetime -from random import getrandbits + from django.conf import settings from django.contrib.auth import authenticate -from evennia.accounts.accounts import CREATION_THROTTLE, LOGIN_THROTTLE from evennia.accounts.models import AccountDB from evennia.objects.models import ObjectDB from evennia.comms.models import ChannelDB @@ -15,7 +14,7 @@ from evennia.server.models import ServerConfig from evennia.server.sessionhandler import SESSIONS from evennia.server.throttle import Throttle -from evennia.utils import create, logger, utils, gametime +from evennia.utils import class_from_module, create, logger, utils, gametime from evennia.commands.cmdhandler import CMD_LOGINSTART COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -27,9 +26,6 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate", MULTISESSION_MODE = settings.MULTISESSION_MODE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE -# Create throttles for too many connections -CONNECTION_THROTTLE = Throttle(limit=5, timeout=1 * 60) - def create_guest_account(session): """ Creates a guest account/character for this session, if one is available. @@ -42,50 +38,20 @@ def create_guest_account(session): the boolean is whether guest accounts are enabled at all. the Account which was created from an available guest name. """ - # check if guests are enabled. - if not settings.GUEST_ENABLED: - return False, None - - # Check IP bans. - bans = ServerConfig.objects.conf("server_bans") - if bans and any(tup[2].match(session.address) for tup in bans if tup[2]): - # this is a banned IP! - string = "|rYou have been banned and cannot continue from here." \ - "\nIf you feel this ban is in error, please email an admin.|x" - session.msg(string) - session.sessionhandler.disconnect(session, "Good bye! Disconnecting.") - return True, None - - try: - # Find an available guest name. - accountname = None - for name in settings.GUEST_LIST: - if not AccountDB.objects.filter(username__iexact=accountname).count(): - accountname = name - break - if not accountname: - session.msg("All guest accounts are in use. Please try again later.") - return True, None - else: - # build a new account with the found guest accountname - password = "%016x" % getrandbits(64) - home = ObjectDB.objects.get_id(settings.GUEST_HOME) - permissions = settings.PERMISSION_GUEST_DEFAULT - typeclass = settings.BASE_CHARACTER_TYPECLASS - ptypeclass = settings.BASE_GUEST_TYPECLASS - new_account = _create_account(session, accountname, password, permissions, ptypeclass) - if new_account: - _create_character(session, new_account, typeclass, home, permissions) - return True, new_account - - except Exception: - # We are in the middle between logged in and -not, so we have - # to handle tracebacks ourselves at this point. If we don't, - # we won't see any errors at all. - session.msg("An error occurred. Please e-mail an admin if the problem persists.") - logger.log_trace() - raise - + enabled = settings.GUEST_ENABLED + address = session.address + + # Get account class + Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) + + # Get an available guest account + # authenticate_guest() handles its own throttling + account, errors = Account.authenticate_guest(ip=address) + if account: + return enabled, account + else: + session.msg("|R%s|n" % '\n'.join(errors)) + return enabled, None def create_normal_account(session, name, password): """ @@ -99,38 +65,17 @@ def create_normal_account(session, name, password): Returns: account (Account): the account which was created from the name and password. """ - # check for too many login errors too quick. + # Get account class + Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) + address = session.address - if isinstance(address, tuple): - address = address[0] - - if LOGIN_THROTTLE.check(address): - session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n") - return None - + # Match account name and check password - account = authenticate(username=name, password=password) - + # authenticate() handles all its own throttling + account, errors = Account.authenticate(username=name, password=password, ip=address, session=session) if not account: # No accountname or password match - session.msg("Incorrect login information given.") - # this just updates the throttle - LOGIN_THROTTLE.update(address) - # calls account hook for a failed login if possible. - account = AccountDB.objects.get_account_from_name(name) - if account: - account.at_failed_login(session) - return None - - # Check IP and/or name bans - bans = ServerConfig.objects.conf("server_bans") - if bans and (any(tup[0] == account.name.lower() for tup in bans) or - any(tup[2].match(session.address) for tup in bans if tup[2])): - # this is a banned IP or name! - string = "|rYou have been banned and cannot continue from here." \ - "\nIf you feel this ban is in error, please email an admin.|x" - session.msg(string) - session.sessionhandler.disconnect(session, "Good bye! Disconnecting.") + session.msg("|R%s|n" % '\n'.join(errors)) return None return account @@ -162,15 +107,10 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS): there is no object yet before the account has logged in) """ session = self.caller - - # check for too many login errors too quick. address = session.address - if isinstance(address, tuple): - address = address[0] - if CONNECTION_THROTTLE.check(address): - # timeout is 5 minutes. - session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n") - return + + # Get account class + Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) args = self.args # extract double quote parts @@ -178,23 +118,27 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS): if len(parts) == 1: # this was (hopefully) due to no double quotes being found, or a guest login parts = parts[0].split(None, 1) + # Guest login if len(parts) == 1 and parts[0].lower() == "guest": - enabled, new_account = create_guest_account(session) - if new_account: - session.sessionhandler.login(session, new_account) - if enabled: + account, errors = Account.authenticate_guest(ip=address) + if account: + session.sessionhandler.login(session, account) return - + else: + session.msg("|R%s|n" % '\n'.join(errors)) + return + if len(parts) != 2: session.msg("\n\r Usage (without <>): connect ") return - CONNECTION_THROTTLE.update(address) name, password = parts - account = create_normal_account(session, name, password) + account, errors = Account.authenticate(username=name, password=password, ip=address, session=session) if account: session.sessionhandler.login(session, account) + else: + session.msg("|R%s|n" % '\n'.join(errors)) class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): @@ -220,14 +164,10 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): session = self.caller args = self.args.strip() - # Rate-limit account creation. address = session.address - - if isinstance(address, tuple): - address = address[0] - if CREATION_THROTTLE.check(address): - session.msg("|RYou are creating too many accounts. Try again in a few minutes.|n") - return + + # Get account class + Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) # extract double quoted parts parts = [part.strip() for part in re.split(r"\"", args) if part.strip()] @@ -239,77 +179,21 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): "\nIf or contains spaces, enclose it in double quotes." session.msg(string) return - accountname, password = parts - - # sanity checks - if not re.findall(r"^[\w. @+\-']+$", accountname) or not (0 < len(accountname) <= 30): - # this echoes the restrictions made by django's auth - # module (except not allowing spaces, for convenience of - # logging in). - string = "\n\r Accountname can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_/' only." - session.msg(string) - return - # strip excessive spaces in accountname - accountname = re.sub(r"\s+", " ", accountname).strip() - if AccountDB.objects.filter(username__iexact=accountname): - # account already exists (we also ignore capitalization here) - session.msg("Sorry, there is already an account with the name '%s'." % accountname) - return - # Reserve accountnames found in GUEST_LIST - if settings.GUEST_LIST and accountname.lower() in (guest.lower() for guest in settings.GUEST_LIST): - string = "\n\r That name is reserved. Please choose another Accountname." - session.msg(string) - return - - # Validate password - Account = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS) - # Have to create a dummy Account object to check username similarity - valid, error = Account.validate_password(password, account=Account(username=accountname)) - if error: - errors = [e for suberror in error.messages for e in error.messages] - string = "\n".join(errors) - session.msg(string) - return - - # Check IP and/or name bans - bans = ServerConfig.objects.conf("server_bans") - if bans and (any(tup[0] == accountname.lower() for tup in bans) or - - any(tup[2].match(session.address) for tup in bans if tup[2])): - # this is a banned IP or name! - string = "|rYou have been banned and cannot continue from here." \ - "\nIf you feel this ban is in error, please email an admin.|x" - session.msg(string) - session.sessionhandler.disconnect(session, "Good bye! Disconnecting.") - return + + username, password = parts # everything's ok. Create the new account account. - try: - permissions = settings.PERMISSION_ACCOUNT_DEFAULT - typeclass = settings.BASE_CHARACTER_TYPECLASS - new_account = _create_account(session, accountname, password, permissions) - if new_account: - if MULTISESSION_MODE < 2: - default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - _create_character(session, new_account, typeclass, default_home, permissions) - - # Update the throttle to indicate a new account was created from this IP - CREATION_THROTTLE.update(address) - - # tell the caller everything went well. - string = "A new account '%s' was created. Welcome!" - if " " in accountname: - string += "\n\nYou can now log in with the command 'connect \"%s\" '." - else: - string += "\n\nYou can now log with the command 'connect %s '." - session.msg(string % (accountname, accountname)) - - except Exception: - # We are in the middle between logged in and -not, so we have - # to handle tracebacks ourselves at this point. If we don't, - # we won't see any errors at all. - session.msg("An error occurred. Please e-mail an admin if the problem persists.") - logger.log_trace() + account, errors = Account.create(username=username, password=password, ip=address, session=session) + if account: + # tell the caller everything went well. + string = "A new account '%s' was created. Welcome!" + if " " in username: + string += "\n\nYou can now log in with the command 'connect \"%s\" '." + else: + string += "\n\nYou can now log with the command 'connect %s '." + session.msg(string % (username, username)) + else: + session.msg("|R%s|n" % '\n'.join(errors)) class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS): diff --git a/evennia/server/validators.py b/evennia/server/validators.py index bccbde6b51..fdadeda6e3 100644 --- a/evennia/server/validators.py +++ b/evennia/server/validators.py @@ -23,7 +23,7 @@ class EvenniaUsernameAvailabilityValidator: """ # Check guest list - if settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.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',