3
0
mirror of https://github.com/jlu5/PyLink.git synced 2024-11-01 09:19:23 +01:00

Rewrite login handling (Closes #590)

* Move identify command and login helpers into coremods.login
   - corecommands._login -> login._irc_try_login
* Add login._get_account() function to consistently fetch login block info
* Rename functions in coremods.login to snake case:
   - checkLogin -> check_login
   - verifyHash -> verify_hash
* Replace explicit returns in login checks with raising utils.NotAuthorizedError()
This commit is contained in:
James Lu 2018-05-25 23:50:55 -07:00
parent 73261a31bd
commit 9e936f1612
2 changed files with 82 additions and 86 deletions

View File

@ -13,74 +13,6 @@ from pylinkirc.log import log
# Essential, core commands go here so that the "commands" plugin with less-important, # Essential, core commands go here so that the "commands" plugin with less-important,
# but still generic functions can be reloaded. # but still generic functions can be reloaded.
def _login(irc, source, username):
"""Internal function to process logins."""
# Mangle case before we start checking for login data.
accounts = {k.lower(): v for k, v in conf.conf['login'].get('accounts', {}).items()}
if irc.is_internal_client(source):
irc.error("Cannot use 'identify' via a command proxy.")
return
logindata = accounts.get(username.lower(), {})
network_filter = logindata.get('networks')
require_oper = logindata.get('require_oper', False)
hosts_filter = logindata.get('hosts', [])
if network_filter and irc.name not in network_filter:
irc.error("You are not authorized to log in to %r on this network." % username)
log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)", irc.name, username, irc.get_hostmask(source), ', '.join(network_filter), irc.name)
return
elif require_oper and not irc.is_oper(source, allowAuthed=False):
irc.error("You must be opered to log in to %r." % username)
log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.get_hostmask(source))
return
elif hosts_filter and not any(irc.match_host(host, source) for host in hosts_filter):
irc.error("Failed to log in to %r: hostname mismatch." % username)
log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.get_hostmask(source))
return
irc.users[source].account = username
irc.reply('Successfully logged in as %s.' % username)
log.info("(%s) Successful login to %r by %s",
irc.name, username, irc.get_hostmask(source))
def _loginfail(irc, source, username):
"""Internal function to process login failures."""
irc.error('Incorrect credentials.')
log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.get_hostmask(source))
def identify(irc, source, args):
"""<username> <password>
Logs in to PyLink using the configured administrator account."""
if irc.is_channel(irc.called_in):
irc.reply('Error: This command must be sent in private. '
'(Would you really type a password inside a channel?)')
return
try:
username, password = args[0], args[1]
except IndexError:
irc.reply('Error: Not enough arguments.')
return
# Process new-style accounts.
if login.checkLogin(username, password):
_login(irc, source, username)
return
# Process legacy logins (login:user).
if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'):
realuser = conf.conf['login']['user']
_login(irc, source, realuser)
else:
# Username not found.
_loginfail(irc, source, username)
utils.add_cmd(identify, aliases=('login', 'id'))
@utils.add_cmd @utils.add_cmd
def shutdown(irc, source, args): def shutdown(irc, source, args):
"""takes no arguments. """takes no arguments.

View File

@ -18,37 +18,39 @@ if CryptContext:
sha256_crypt__default_rounds=180000, sha256_crypt__default_rounds=180000,
sha512_crypt__default_rounds=90000) sha512_crypt__default_rounds=90000)
def checkLogin(user, password): def _get_account(accountname):
"""Checks whether the given user and password is a valid combination.""" """
accounts = conf.conf['login'].get('accounts') Returns the login data block for the given account name (case-insensitive), or False if none
if not accounts: exists.
# No accounts specified, return. """
return False accounts = {k.lower(): v for k, v in
conf.conf['login'].get('accounts', {}).items()}
# Lowercase account names to make them case insensitive. TODO: check for
# duplicates.
user = user.lower()
accounts = {k.lower(): v for k, v in accounts.items()}
try: try:
account = accounts[user] return accounts[accountname.lower()]
except KeyError: # Invalid combination except KeyError:
return False return False
else:
def check_login(user, password):
"""Checks whether the given user and password is a valid combination."""
account = _get_account(user)
if account:
passhash = account.get('password') passhash = account.get('password')
if not passhash: if not passhash:
# No password given, return. XXX: we should allow plugins to override # No password given, return. XXX: we should allow plugins to override
# this in the future. # this in the future.
return False return False
# Encryption in account passwords is optional (to not break backwards # Hashing in account passwords is optional.
# compatibility).
if account.get('encrypted', False): if account.get('encrypted', False):
return verifyHash(password, passhash) return verify_hash(password, passhash)
else: else:
return password == passhash return password == passhash
def verifyHash(password, passhash): return False
def verify_hash(password, passhash):
"""Checks whether the password given matches the hash.""" """Checks whether the password given matches the hash."""
if password: if password:
if not pwd_context: if not pwd_context:
@ -57,3 +59,65 @@ def verifyHash(password, passhash):
return pwd_context.verify(password, passhash) return pwd_context.verify(password, passhash)
return False # No password given! return False # No password given!
def _irc_try_login(irc, source, username):
"""Internal function to process logins via IRC."""
if irc.is_internal_client(source):
irc.error("Cannot use 'identify' via a command proxy.")
return
logindata = _get_account(username)
network_filter = logindata.get('networks')
require_oper = logindata.get('require_oper', False)
hosts_filter = logindata.get('hosts', [])
if network_filter and irc.name not in network_filter:
log.warning("(%s) Failed login to %r from %s (wrong network: networks filter says %r but we got %r)",
irc.name, username, irc.get_hostmask(source), ', '.join(network_filter), irc.name)
raise utils.NotAuthorizedError("Account is not authorized to login on this network.")
elif require_oper and not irc.is_oper(source, allowAuthed=False):
log.warning("(%s) Failed login to %r from %s (needs oper)", irc.name, username, irc.get_hostmask(source))
raise utils.NotAuthorizedError("You must be opered.")
elif hosts_filter and not any(irc.match_host(host, source) for host in hosts_filter):
log.warning("(%s) Failed login to %r from %s (hostname mismatch)", irc.name, username, irc.get_hostmask(source))
raise utils.NotAuthorizedError("Hostname mismatch.")
irc.users[source].account = username
irc.reply('Successfully logged in as %s.' % username)
log.info("(%s) Successful login to %r by %s",
irc.name, username, irc.get_hostmask(source))
return True
def identify(irc, source, args):
"""<username> <password>
Logs in to PyLink using the configured administrator account."""
if irc.is_channel(irc.called_in):
irc.reply('Error: This command must be sent in private. '
'(Would you really type a password inside a channel?)')
return
try:
username, password = args[0], args[1]
except IndexError:
irc.reply('Error: Not enough arguments.')
return
# Process new-style accounts.
if check_login(username, password):
_irc_try_login(irc, source, username)
return
# Process legacy logins (login:user).
if username.lower() == conf.conf['login'].get('user', '').lower() and password == conf.conf['login'].get('password'):
realuser = conf.conf['login']['user']
_irc_try_login(irc, source, realuser)
return
# Username not found or password incorrect.
log.warning("(%s) Failed login to %r from %s", irc.name, username, irc.get_hostmask(source))
raise utils.NotAuthorizedError('Bad username or password.')
utils.add_cmd(identify, aliases=('login', 'id'))